Give builder fields persistent labels, treat description as optional, and validate save-progress email with a real form.
Name stays required; description is a labelled optional textarea. Email uses native validity and form submit. Drop the dead workshop link and fix a few copy errors. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE,
|
||||
} from "./utils/flowSteps";
|
||||
import {
|
||||
CREATE_FLOW_COMMUNITY_SAVE_FORM_ID,
|
||||
CREATE_FLOW_SYNC_DRAFT_QUERY,
|
||||
CREATE_FLOW_SYNC_DRAFT_VALUE,
|
||||
CREATE_ROUTES,
|
||||
@@ -482,12 +483,28 @@ function CreateFlowLayoutContent({
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [currentStep, sessionResolved, sessionUser, openLogin]);
|
||||
|
||||
const handleCommunitySaveMagicLinkSubmit = useCallback(async () => {
|
||||
const handleCommunitySaveMagicLinkSubmit = useCallback(async (
|
||||
formEl?: HTMLFormElement | null,
|
||||
) => {
|
||||
setCommunitySaveMagicLinkError(null);
|
||||
setCommunitySaveMagicLinkSuccess(false);
|
||||
const raw = state.communitySaveEmail;
|
||||
const trimmed = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||
if (!isValidCreateFlowSaveEmail(trimmed)) return;
|
||||
const namedEmail = formEl?.elements.namedItem("email");
|
||||
const emailField =
|
||||
namedEmail instanceof HTMLInputElement
|
||||
? namedEmail
|
||||
: typeof document === "undefined"
|
||||
? null
|
||||
: document.querySelector<HTMLInputElement>(
|
||||
`input[name="email"][form="${CREATE_FLOW_COMMUNITY_SAVE_FORM_ID}"]`,
|
||||
);
|
||||
if (emailField != null && !emailField.checkValidity()) {
|
||||
emailField.reportValidity();
|
||||
return;
|
||||
}
|
||||
const raw =
|
||||
emailField != null ? emailField.value : state.communitySaveEmail;
|
||||
if (typeof raw !== "string" || !isValidCreateFlowSaveEmail(raw)) return;
|
||||
const trimmed = raw.trim().toLowerCase();
|
||||
|
||||
setCommunitySaveMagicLinkSubmitting(true);
|
||||
try {
|
||||
@@ -770,6 +787,16 @@ function CreateFlowLayoutContent({
|
||||
isCompletedStep ? "!bg-[var(--color-teal-teal50,#c9fef9)]" : ""
|
||||
}`.trim()}
|
||||
/>
|
||||
{currentStep === "community-save" ? (
|
||||
<form
|
||||
id={CREATE_FLOW_COMMUNITY_SAVE_FORM_ID}
|
||||
className="hidden"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void handleCommunitySaveMagicLinkSubmit(e.currentTarget);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<main
|
||||
className={`flex min-h-0 min-w-0 flex-1 w-full max-w-full overflow-x-hidden ${mainContentClass} ${mainResponsiveLayout}`}
|
||||
>
|
||||
@@ -841,6 +868,8 @@ function CreateFlowLayoutContent({
|
||||
{footer.saveLater}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form={CREATE_FLOW_COMMUNITY_SAVE_FORM_ID}
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
@@ -851,9 +880,6 @@ function CreateFlowLayoutContent({
|
||||
!isValidCreateFlowSaveEmail(state.communitySaveEmail)
|
||||
}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
void handleCommunitySaveMagicLinkSubmit();
|
||||
}}
|
||||
>
|
||||
{communitySaveMagicLinkSubmitting
|
||||
? footer.submitEmailSending
|
||||
|
||||
@@ -80,6 +80,8 @@ function CustomMethodCardWizardViewComponent({
|
||||
<TextArea
|
||||
appearance="default"
|
||||
formHeader={false}
|
||||
showHelpIcon={false}
|
||||
label={copy.step2.title}
|
||||
placeholder={copy.step2.fieldPlaceholder}
|
||||
value={policyDescription}
|
||||
maxLength={maxDescriptionChars}
|
||||
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, useId } from "react";
|
||||
import { ASSETS, getAssetPath } from "../../../../../lib/assetUtils";
|
||||
import InputWithCounter from "../../../../components/controls/InputWithCounter";
|
||||
import TextArea from "../../../../components/controls/TextArea";
|
||||
@@ -39,6 +39,7 @@ function CustomMethodCardWizardFieldBodiesViewComponent({
|
||||
onProportionBlockTitleChange,
|
||||
onProportionDefaultChange,
|
||||
}: CustomMethodCardWizardFieldBodiesViewProps) {
|
||||
const placeholderFieldId = useId();
|
||||
const uploadPreviewTrimmed = uploadAssetPreviewUrl?.trim() ?? "";
|
||||
const hasUploadPreview = uploadPreviewTrimmed.length > 0;
|
||||
|
||||
@@ -53,10 +54,14 @@ function CustomMethodCardWizardFieldBodiesViewComponent({
|
||||
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[14px] leading-[20px] font-medium text-[var(--color-content-default-primary)]">
|
||||
<label
|
||||
htmlFor={placeholderFieldId}
|
||||
className="text-[14px] leading-[20px] font-medium text-[var(--color-content-default-primary)]"
|
||||
>
|
||||
{copy.text.placeholderLabel}
|
||||
</label>
|
||||
<TextArea
|
||||
id={placeholderFieldId}
|
||||
formHeader={false}
|
||||
appearance="embedded"
|
||||
value={textPlaceholderBody}
|
||||
@@ -84,6 +89,7 @@ function CustomMethodCardWizardFieldBodiesViewComponent({
|
||||
/>
|
||||
<TextInput
|
||||
formHeader={false}
|
||||
label={copy.badges.blockTitleLabel}
|
||||
placeholder={copy.badges.blockTitlePlaceholder}
|
||||
value={badgeBlockTitle}
|
||||
onChange={(e) => onBadgeBlockTitleChange(e.target.value)}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useBeforeUnloadGuard } from "../../../hooks/useBeforeUnloadGuard";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import TextInput from "../../../components/controls/TextInput";
|
||||
import TextArea from "../../../components/controls/TextArea";
|
||||
import ContentLockup from "../../../components/type/ContentLockup";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
|
||||
@@ -95,18 +95,20 @@ export function FinalReviewCommunityContextEditModal({
|
||||
ariaLabel={tModal("title")}
|
||||
>
|
||||
<div className="pb-2">
|
||||
<TextInput
|
||||
<TextArea
|
||||
className="!transition-none"
|
||||
type="text"
|
||||
label={tModal("title")}
|
||||
placeholder={tField("placeholder")}
|
||||
value={draft}
|
||||
onChange={(e) => {
|
||||
setDraft(e.target.value);
|
||||
}}
|
||||
inputSize="medium"
|
||||
size="medium"
|
||||
formHeader={false}
|
||||
showHelpIcon={false}
|
||||
textHint={characterHint}
|
||||
maxLength={COMMUNITY_CONTEXT_FIELD_MAX_LENGTH}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</Create>
|
||||
|
||||
@@ -93,6 +93,7 @@ export function FinalReviewTitleEditModal({
|
||||
<TextInput
|
||||
className="!transition-none"
|
||||
type="text"
|
||||
label={tModal("title")}
|
||||
placeholder={tField("placeholder")}
|
||||
value={draft}
|
||||
onChange={(e) => {
|
||||
@@ -100,8 +101,10 @@ export function FinalReviewTitleEditModal({
|
||||
}}
|
||||
inputSize="medium"
|
||||
formHeader={false}
|
||||
showHelpIcon={false}
|
||||
textHint={characterHint}
|
||||
maxLength={COMMUNITY_TITLE_FIELD_MAX_LENGTH}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</Create>
|
||||
|
||||
@@ -115,6 +115,7 @@ export function renderCreateFlowScreen(screenId: CreateFlowStep): ReactNode {
|
||||
messageNamespace="create.community.communityName"
|
||||
stateField="title"
|
||||
maxLength={48}
|
||||
required
|
||||
/>
|
||||
);
|
||||
case "community-structure":
|
||||
@@ -126,6 +127,7 @@ export function renderCreateFlowScreen(screenId: CreateFlowStep): ReactNode {
|
||||
stateField="communityContext"
|
||||
maxLength={200}
|
||||
mainAlign="center"
|
||||
multiline
|
||||
/>
|
||||
);
|
||||
case "community-size":
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import NumberedList from "../../../../components/type/NumberedList";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
@@ -31,22 +30,6 @@ export function InformationalScreen() {
|
||||
},
|
||||
];
|
||||
|
||||
const description: ReactNode = (
|
||||
<>
|
||||
{copy.descriptionLead}{" "}
|
||||
<a
|
||||
href="#"
|
||||
className="font-normal text-[var(--color-content-default-tertiary,#b4b4b4)] underline decoration-solid underline-offset-[3px] cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{copy.workshopLabel}
|
||||
</a>{" "}
|
||||
{copy.descriptionTrail}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<CreateFlowStepShell
|
||||
variant="centeredNarrow"
|
||||
@@ -57,7 +40,7 @@ export function InformationalScreen() {
|
||||
>
|
||||
<CreateFlowHeaderLockup
|
||||
title={copy.title}
|
||||
description={description}
|
||||
description={copy.description}
|
||||
justification="left"
|
||||
/>
|
||||
<NumberedList items={items} size={mdUp ? "M" : "S"} />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, type HTMLInputTypeAttribute } from "react";
|
||||
import TextInput from "../../../../components/controls/TextInput";
|
||||
import TextArea from "../../../../components/controls/TextArea";
|
||||
import type { HeaderLockupJustificationValue } from "../../../../components/type/HeaderLockup/HeaderLockup.types";
|
||||
import { useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
type CreateFlowContentTopBelowMd,
|
||||
} from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
import { CREATE_FLOW_COMMUNITY_SAVE_FORM_ID } from "../../utils/createFlowPaths";
|
||||
import type { CreateFlowTextStateField } from "../../types";
|
||||
|
||||
type Props = {
|
||||
@@ -21,6 +23,10 @@ type Props = {
|
||||
/** Figma Flow — Text (`20094:41243`): main column `items-center` + horizontal padding token. */
|
||||
mainAlign?: "start" | "center";
|
||||
inputType?: HTMLInputTypeAttribute;
|
||||
/** Multi-line control (community description). */
|
||||
multiline?: boolean;
|
||||
/** Native `required` (community name and save-progress email). */
|
||||
required?: boolean;
|
||||
showCharacterCount?: boolean;
|
||||
headerJustification?: HeaderLockupJustificationValue;
|
||||
/** Top spacing under top chrome (`CreateFlowStepShell` / `CreateFlowContentTopBelowMd`). */
|
||||
@@ -28,7 +34,7 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared narrow-column + TextInput pattern for Create Community text frames.
|
||||
* Shared narrow-column + labelled field pattern for Create Community text frames.
|
||||
*/
|
||||
export function CreateFlowTextFieldScreen({
|
||||
messageNamespace,
|
||||
@@ -36,6 +42,8 @@ export function CreateFlowTextFieldScreen({
|
||||
maxLength,
|
||||
mainAlign = "start",
|
||||
inputType = "text",
|
||||
multiline = false,
|
||||
required = false,
|
||||
showCharacterCount = true,
|
||||
headerJustification = "left",
|
||||
contentTopBelowMd = "space-1400",
|
||||
@@ -69,6 +77,15 @@ export function CreateFlowTextFieldScreen({
|
||||
const mainItems =
|
||||
mainAlign === "center" ? "items-center" : "items-start";
|
||||
|
||||
const isEmail = inputType === "email";
|
||||
const persistValue = (next: string) => {
|
||||
setValue(next);
|
||||
markCreateFlowInteraction();
|
||||
updateState({ [stateField]: next } as Record<string, string>);
|
||||
};
|
||||
|
||||
const fieldLabel = t("inputLabel");
|
||||
|
||||
return (
|
||||
<CreateFlowStepShell
|
||||
variant="centeredNarrow"
|
||||
@@ -85,22 +102,60 @@ export function CreateFlowTextFieldScreen({
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<TextInput
|
||||
className="!transition-none"
|
||||
type={inputType}
|
||||
placeholder={t("placeholder")}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
setValue(v);
|
||||
markCreateFlowInteraction();
|
||||
updateState({ [stateField]: v } as Record<string, string>);
|
||||
}}
|
||||
inputSize={mdUp ? "medium" : "small"}
|
||||
formHeader={false}
|
||||
textHint={hint}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
{multiline ? (
|
||||
<TextArea
|
||||
className="!transition-none"
|
||||
label={fieldLabel}
|
||||
placeholder={t("placeholder")}
|
||||
value={value}
|
||||
onChange={(e) => persistValue(e.target.value)}
|
||||
onBlur={() => {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed !== value) persistValue(trimmed);
|
||||
}}
|
||||
size={mdUp ? "medium" : "small"}
|
||||
formHeader={false}
|
||||
showHelpIcon={false}
|
||||
textHint={hint}
|
||||
maxLength={maxLength}
|
||||
required={required}
|
||||
rows={4}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
className="!transition-none"
|
||||
type={inputType}
|
||||
name={isEmail ? "email" : undefined}
|
||||
label={fieldLabel}
|
||||
placeholder={t("placeholder")}
|
||||
value={value}
|
||||
onChange={(e) => persistValue(e.target.value)}
|
||||
onBlur={() => {
|
||||
const trimmed = isEmail
|
||||
? value.trim().toLowerCase()
|
||||
: value.trim();
|
||||
if (trimmed !== value) persistValue(trimmed);
|
||||
}}
|
||||
onKeyDown={
|
||||
isEmail
|
||||
? (e) => {
|
||||
if (e.key !== "Enter") return;
|
||||
e.preventDefault();
|
||||
e.currentTarget.form?.requestSubmit();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
inputSize={mdUp ? "medium" : "small"}
|
||||
formHeader={false}
|
||||
showHelpIcon={false}
|
||||
textHint={hint}
|
||||
maxLength={maxLength}
|
||||
required={required || isEmail}
|
||||
autoComplete={isEmail ? "email" : undefined}
|
||||
inputMode={isEmail ? "email" : undefined}
|
||||
form={isEmail ? CREATE_FLOW_COMMUNITY_SAVE_FORM_ID : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CreateFlowStepShell>
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
FIRST_STEP,
|
||||
} from "./flowSteps";
|
||||
|
||||
/** Associates the save-progress email field with the layout footer submit. */
|
||||
export const CREATE_FLOW_COMMUNITY_SAVE_FORM_ID =
|
||||
"create-flow-community-save";
|
||||
|
||||
export const CREATE_ROUTES = {
|
||||
root: "/",
|
||||
createRoot: "/create",
|
||||
|
||||
Reference in New Issue
Block a user