Walk Create Community through the Figma screens and restore a chosen logo after reload, so a guest can leave without saving until they reach the email step.

This commit is contained in:
adilallo committed 2026-11-05 16:14:04 -07:00
1 parent ca1f58a50c
commit 2348a9e94f
61 files changed
+1110 -421

No files matched your search

+3 -3
View File
@@ -168,7 +168,7 @@ export function RuleView({
logoUrl.startsWith("http://localhost") ||
logoUrl.startsWith("https://localhost");
const containerClass = `${logoContainerClass} relative rounded-full overflow-hidden mix-blend-luminosity`;
const containerClass = `${logoContainerClass} relative overflow-hidden rounded-full bg-[var(--color-surface-invert-brand-teal)]`;
if (isLocalhost) {
return (
@@ -179,7 +179,7 @@ export function RuleView({
alt={logoAlt || title}
width={logoSize}
height={logoSize}
className="w-full h-full object-cover rounded-full"
className="h-full w-full rounded-full object-cover opacity-70 mix-blend-color-burn"
/>
</div>
);
@@ -192,7 +192,7 @@ export function RuleView({
alt={logoAlt || title}
width={logoSize}
height={logoSize}
className="w-full h-full object-cover rounded-full"
className="h-full w-full rounded-full object-cover opacity-70 mix-blend-color-burn"
/>
</div>
);
@@ -21,6 +21,8 @@ const MultiSelectContainer = memo<MultiSelectProps>(
addButton: addButtonProp = true,
addButtonText,
formHeader = true,
helperText,
errorMessage,
onCustomChipConfirm,
onCustomChipClose,
maxSelections,
@@ -49,6 +51,8 @@ const MultiSelectContainer = memo<MultiSelectProps>(
chipSelectAriaLabel={t("chipSelect")}
chipDeselectAriaLabel={t("chipDeselect")}
formHeader={formHeader}
helperText={helperText}
errorMessage={errorMessage}
onCustomChipConfirm={onCustomChipConfirm}
onCustomChipClose={onCustomChipClose}
maxSelections={maxSelections}
@@ -54,6 +54,15 @@ export interface MultiSelectProps {
* @default true
*/
formHeader?: boolean;
/**
* Helper line beside the label (Figma “Pick one”). Wins over
* `selectionCountText` in the header.
*/
helperText?: string;
/**
* Danger hint under the chips for a required question left unanswered.
*/
errorMessage?: string;
/**
* Callback when a custom chip is confirmed (check button clicked)
*/
@@ -88,6 +97,8 @@ export interface MultiSelectViewProps {
chipSelectAriaLabel: string;
chipDeselectAriaLabel: string;
formHeader: boolean;
helperText?: string;
errorMessage?: string;
onCustomChipConfirm?: (chipId: string, value: string) => void;
onCustomChipClose?: (chipId: string) => void;
maxSelections?: number;
@@ -19,6 +19,8 @@ function MultiSelectView({
chipSelectAriaLabel,
chipDeselectAriaLabel,
formHeader = true,
helperText: helperTextProp,
errorMessage,
onCustomChipConfirm,
onCustomChipClose,
maxSelections,
@@ -39,7 +41,9 @@ function MultiSelectView({
const atLimit =
maxSelections != null && selectedCount >= maxSelections;
const countCaption = selectionCountText?.trim() ?? "";
const helperText = countCaption.length > 0 ? countCaption : false;
const headerHelper =
helperTextProp?.trim() ||
(countCaption.length > 0 ? countCaption : false);
return (
<div
@@ -51,16 +55,16 @@ function MultiSelectView({
label={label}
helpIcon={showHelpIcon}
asterisk={false}
helperText={helperText}
helperText={headerHelper}
size={size}
palette={palette}
/>
)}
{!formHeader || !label ? (
countCaption.length > 0 ? (
headerHelper && typeof headerHelper === "string" ? (
<p className="w-full text-x-small-paragraph text-[color:var(--color-content-default-tertiary)]">
{countCaption}
{headerHelper}
</p>
) : null
) : null}
@@ -177,6 +181,14 @@ function MultiSelectView({
</button>
)}
</div>
{errorMessage ? (
<p
className="w-full text-x-small-paragraph text-[color:var(--color-content-default-negative-primary)]"
role="alert"
>
{errorMessage}
</p>
) : null}
</div>
);
}
@@ -71,5 +71,6 @@ export interface TextInputViewProps {
inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
required?: boolean;
form?: string;
autoFocus?: boolean;
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
}
@@ -35,6 +35,7 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
inputMode,
required = false,
form,
autoFocus,
onKeyDown,
state: _state,
isFilled: _isFilled,
@@ -94,6 +95,7 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
inputMode={inputMode}
required={required}
form={form}
autoFocus={autoFocus}
aria-invalid={error || undefined}
aria-required={required || undefined}
className={inputClasses}
@@ -18,12 +18,17 @@ const UploadContainer = memo<UploadProps>(
label,
showHelpIcon = true,
hintText,
buttonLabel,
previewSrc,
previewAlt,
fileName,
onClick,
className = "",
}) => {
const t = useTranslation("controlsChrome");
const resolvedState: UploadStateValue =
state ?? (active ? "default" : "disabled");
const uploadButtonLabel = buttonLabel ?? t("uploadButton");
return (
<UploadView
@@ -32,8 +37,11 @@ const UploadContainer = memo<UploadProps>(
label={label}
showHelpIcon={showHelpIcon}
hintText={hintText ?? t("uploadHintDefault")}
uploadButtonLabel={t("uploadButton")}
uploadAriaLabel={t("uploadAriaLabel")}
uploadButtonLabel={uploadButtonLabel}
uploadAriaLabel={uploadButtonLabel}
previewSrc={previewSrc}
previewAlt={previewAlt ?? t("uploadAriaLabel")}
fileName={fileName}
onClick={onClick}
className={className}
/>
@@ -32,6 +32,17 @@ export interface UploadProps {
* @default "Add image from your device"
*/
hintText?: string;
/**
* Label on the upload control. Defaults to the chrome “Upload” string.
* Pass “Replace” for the uploaded state.
*/
buttonLabel?: string;
/**
* Round preview of the selected file (Figma Has Preview).
*/
previewSrc?: string;
previewAlt?: string;
fileName?: string;
/**
* Callback when upload button is clicked
*/
@@ -50,6 +61,9 @@ export interface UploadViewProps {
hintText: string;
uploadButtonLabel: string;
uploadAriaLabel: string;
previewSrc?: string;
previewAlt: string;
fileName?: string;
onClick?: () => void;
className: string;
}
+62 -47
View File
@@ -12,13 +12,17 @@ function UploadView({
hintText,
uploadButtonLabel,
uploadAriaLabel,
previewSrc,
previewAlt,
fileName,
onClick,
className = "",
}: UploadViewProps) {
const isActive = state !== "disabled";
const isDisabled = state === "disabled";
const hasPreview =
typeof previewSrc === "string" && previewSrc.length > 0;
// Button styles based on active state
const buttonBgClass = isActive
? "bg-[var(--color-surface-invert-primary)]"
: "bg-[var(--color-surface-default-secondary)]";
@@ -27,12 +31,10 @@ function UploadView({
? "text-[color:var(--color-content-invert-primary)]"
: "text-[color:var(--color-content-invert-tertiary)]";
// Description text color based on active state
const descriptionTextColor = isActive
? "text-[color:var(--color-content-default-primary)]"
: "text-[color:var(--color-content-default-tertiary)]";
// Icon color based on active state
const iconColor = isActive
? "text-[color:var(--color-content-invert-primary)]"
: "text-[color:var(--color-content-invert-tertiary)]";
@@ -41,7 +43,6 @@ function UploadView({
<div
className={`flex flex-col gap-[var(--space-300)] items-start relative w-full ${className}`}
>
{/* Label using InputLabel component */}
{hasLabel && label ? (
<InputLabel
label={label}
@@ -53,15 +54,27 @@ function UploadView({
/>
) : null}
{/* Upload container — max width per create-flow spec */}
<div
className={`bg-[var(--color-surface-default-secondary)] mx-auto flex w-full max-w-[474px] shrink-0 items-center justify-center gap-[24px] rounded-[var(--radius-200)] px-[var(--space-600)] py-[var(--space-1200)] ${
state === "error"
? "border border-[var(--color-border-default-utility-negative)]"
? "border-2 border-[var(--color-border-default-negative-primary)]"
: ""
} ${state === "focus" ? "shadow-[var(--shadow-focus-default)]" : ""}`}
>
{/* Upload button */}
{hasPreview ? (
<div
className="relative size-[80px] shrink-0 overflow-hidden rounded-full bg-[var(--color-surface-invert-brand-teal)]"
aria-hidden={false}
>
{/* eslint-disable-next-line @next/next/no-img-element -- user/device file or same-origin upload URL */}
<img
src={previewSrc}
alt={previewAlt}
className="h-full w-full object-cover opacity-70 mix-blend-color-burn"
/>
</div>
) : null}
<button
type="button"
onClick={onClick}
@@ -71,43 +84,43 @@ function UploadView({
} ${state === "active" ? "scale-[0.98]" : ""}`}
aria-label={uploadAriaLabel}
>
{/* Upload icon */}
<div className={`relative shrink-0 size-[20px] ${iconColor}`}>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-full"
>
<path
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<polyline
points="17 8 12 3 7 8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<line
x1="12"
y1="3"
x2="12"
y2="15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
{/* Button text */}
{hasPreview ? null : (
<div className={`relative shrink-0 size-[20px] ${iconColor}`}>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-full"
>
<path
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<polyline
points="17 8 12 3 7 8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<line
x1="12"
y1="3"
x2="12"
y2="15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
)}
<div
className={`flex flex-col font-medium justify-center leading-[0] relative shrink-0 text-[length:var(--sizing-400)] whitespace-nowrap ${buttonTextColor}`}
>
@@ -115,11 +128,13 @@ function UploadView({
</div>
</button>
{/* Description text */}
<div
className={`flex flex-[1_0_0] flex-col font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350)] ${descriptionTextColor}`}
className={`flex flex-[1_0_0] flex-col font-normal min-h-[32px] justify-center leading-[0] min-w-px relative text-[length:var(--sizing-350)] ${descriptionTextColor}`}
role={state === "error" ? "alert" : undefined}
>
<p className="leading-[20px] whitespace-pre-wrap">{hintText}</p>
<p className="leading-[20px] whitespace-pre-wrap">
{hasPreview && fileName ? fileName : hintText}
</p>
</div>
</div>
</div>
@@ -73,6 +73,8 @@ const AlertContainer = memo<AlertProps>(
hasBodyText = true,
hasTrailingIcon: hasTrailingIconProp,
onClose,
actionLabel,
onAction,
className = "",
}) => {
const t = useTranslation("controlsChrome");
@@ -178,6 +180,8 @@ const AlertContainer = memo<AlertProps>(
closeButtonIconColor={statusStyles.closeButtonIconColor}
onClose={onClose}
closeAlertAriaLabel={t("closeAlert")}
actionLabel={actionLabel}
onAction={onAction}
/>
);
},
@@ -38,6 +38,9 @@ export interface AlertProps {
*/
hasTrailingIcon?: boolean;
onClose?: () => void;
/** Optional trailing action (Figma “Send it again”). */
actionLabel?: string;
onAction?: () => void;
className?: string;
}
@@ -58,4 +61,6 @@ export interface AlertViewProps {
closeButtonIconColor: string;
onClose?: () => void;
closeAlertAriaLabel: string;
actionLabel?: string;
onAction?: () => void;
}
@@ -18,6 +18,8 @@ export function AlertView({
closeButtonIconColor,
onClose,
closeAlertAriaLabel,
actionLabel,
onAction,
}: AlertViewProps) {
const getIcon = () => {
// Use the Icon_Alert.svg with dynamic fill color
@@ -55,6 +57,15 @@ export function AlertView({
{hasBodyText && description && (
<p className={descriptionClasses}>{description}</p>
)}
{actionLabel && onAction ? (
<button
type="button"
onClick={onAction}
className="mt-[var(--space-100)] cursor-pointer text-small-label text-[color:var(--color-content-invert-primary)] underline underline-offset-[3px]"
>
{actionLabel}
</button>
) : null}
</div>
{hasTrailingIcon && onClose ? (
<Button
@@ -7,6 +7,9 @@ export type ProportionBarState =
| "1-3"
| "1-4"
| "1-5"
| "1-6"
| "1-7"
| "1-8"
| "2-0"
| "2-1"
| "2-2"
@@ -27,13 +27,13 @@ export function ProportionBarView({
// Proportion bar type
const [fullSegments, partialSegment] = progress.split("-").map(Number);
// Calculate total progress:
// - For 1-X: first section is (X+1)/6 filled
// - For 1-X: first section is (X+1)/9 filled (intro 1-0 through review 1-8)
// - For 2-X: first section full, second section filled per Figma ratios (see `SECOND_SEGMENT_FILL_RATIO`)
// - For 3-X: first two sections full, third section X/3 filled
// Max is 3 full segments = 9 units
let totalProgress = 0;
if (fullSegments === 1) {
totalProgress = (partialSegment + 1) / 6; // 1/6 to 6/6 of first section
totalProgress = (partialSegment + 1) / 9; // 1/9 to 9/9 of first section
} else if (fullSegments === 2) {
totalProgress = 1 + getSecondSegmentFillRatio(partialSegment);
} else if (fullSegments === 3) {
@@ -53,7 +53,7 @@ export function ProportionBarView({
? `Progress: ${fullSegments} segments, ${partialSegment} of 3 parts filled (${progressPercentage}%)`
: fullSegments === 2
? `Progress: ${fullSegments} segments, ${partialSegment} of 3 parts filled (${progressPercentage}%)`
: `Progress: ${fullSegments} segment, ${partialSegment + 1} of 6 parts filled (${progressPercentage}%)`;
: `Progress: ${fullSegments} segment, ${partialSegment + 1} of 9 parts filled (${progressPercentage}%)`;
return (
<div
@@ -78,12 +78,12 @@ export function ProportionBarView({
background capsule.
*/}
<div className="absolute inset-0 flex gap-[var(--space-200)] px-[4px] overflow-hidden">
{/* First section - for 1-X: (X+1)/6 filled, for 2-X and 3-X: fully filled */}
{/* First section - for 1-X: (X+1)/9 filled, for 2-X and 3-X: fully filled */}
<div className="flex-1 h-full relative">
{fullSegments === 1 ? (
<div
className="absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)] rounded-l-[var(--radius-full)]"
style={{ width: `${((partialSegment + 1) / 6) * 100}%` }}
style={{ width: `${((partialSegment + 1) / 9) * 100}%` }}
/>
) : fullSegments >= 2 ? (
<div className="absolute inset-0 bg-[var(--color-content-default-brand-primary)] rounded-l-[var(--radius-full)]" />
@@ -2,7 +2,7 @@ export type NumberedListSizeValue = "M" | "S";
export interface NumberedListItem {
title: string;
description: string;
description?: string;
}
export interface NumberedListProps {
@@ -45,16 +45,17 @@ function NumberedListView({ items, size }: NumberedListViewProps) {
</h3>
</div>
{/* Description */}
<p
className={`max-w-[640px] overflow-hidden relative shrink-0 text-[var(--color-content-default-tertiary,#b4b4b4)] text-ellipsis w-full whitespace-pre-wrap ${
isM
? "text-small-paragraph"
: "text-x-small-paragraph"
}`}
>
{item.description}
</p>
{item.description ? (
<p
className={`max-w-[640px] overflow-hidden relative shrink-0 text-[var(--color-content-default-tertiary,#b4b4b4)] text-ellipsis w-full whitespace-pre-wrap ${
isM
? "text-small-paragraph"
: "text-x-small-paragraph"
}`}
>
{item.description}
</p>
) : null}
</div>
</li>
))}