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:
1 parent
ca1f58a50c
commit
2348a9e94f
61 files changed
+1110
-421
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user