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:
adilallo
2026-09-10 11:37:41 -06:00
co-authored by Cursor
parent 130663f930
commit 42d83c8b62
33 changed files with 437 additions and 125 deletions
@@ -1,3 +1,6 @@
"use client";
import { useId } from "react";
import type { InputWithCounterProps } from "./InputWithCounter.types";
export function InputWithCounterView({
@@ -10,12 +13,16 @@ export function InputWithCounterView({
className = "",
inputClassName = "",
}: InputWithCounterProps) {
const inputId = useId();
return (
<div className={`space-y-[var(--spacing-scale-008)] ${className}`}>
{/* Label with help icon */}
{label && (
<div className="flex items-center gap-[var(--spacing-scale-002)]">
<label className="text-[14px] leading-[20px] font-medium text-[var(--color-content-default-primary)]">
<label
htmlFor={inputId}
className="text-[14px] leading-[20px] font-medium text-[var(--color-content-default-primary)]"
>
{label}
</label>
{showHelpIcon && (
@@ -52,6 +59,7 @@ export function InputWithCounterView({
{/* Input field */}
<div className="relative">
<input
id={inputId}
type="text"
placeholder={placeholder}
value={value}
@@ -36,31 +36,39 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
},
ref,
) => {
const labelEl = label ? (
formHeader ? (
<div className="flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative shrink-0 w-full">
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
<label
id={labelId}
htmlFor={textareaId}
className={`${labelClasses} font-medium text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
{showHelpIcon && (
<div className="relative shrink-0 size-[12px]">
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt={helpIconAlt}
className="block max-w-none size-full"
/>
</div>
)}
</div>
</div>
) : (
<label id={labelId} htmlFor={textareaId} className="sr-only">
{label}
</label>
)
) : null;
return (
<div className={containerClasses}>
{formHeader && label && (
<div className="flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative shrink-0 w-full">
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
<label
id={labelId}
htmlFor={textareaId}
className={`${labelClasses} font-medium text-[var(--color-content-default-secondary)]`}
>
{label}
</label>
{showHelpIcon && (
<div className="relative shrink-0 size-[12px]">
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt={helpIconAlt}
className="block max-w-none size-full"
/>
</div>
)}
</div>
</div>
)}
{labelEl}
<div className={disabled ? "opacity-40" : ""}>
<textarea
ref={ref}
@@ -31,6 +31,11 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
textHint = false,
formHeader = true,
maxLength,
autoComplete,
inputMode,
required = false,
form,
onKeyDown,
...props
},
ref,
@@ -250,6 +255,11 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
maxLength={maxLength}
helpIconAlt={t("helpIconAlt")}
hintDefault={t("hintDefault")}
autoComplete={autoComplete}
inputMode={inputMode}
required={required}
form={form}
onKeyDown={onKeyDown}
{...props}
/>
);
@@ -67,4 +67,9 @@ export interface TextInputViewProps {
maxLength?: number;
helpIconAlt: string;
hintDefault: string;
autoComplete?: string;
inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
required?: boolean;
form?: string;
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
}
@@ -13,7 +13,7 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
name,
type,
disabled,
error: _error,
error = false,
className: _className,
containerClasses,
labelClasses,
@@ -31,34 +31,49 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
maxLength,
helpIconAlt,
hintDefault,
autoComplete,
inputMode,
required = false,
form,
onKeyDown,
state: _state,
isFilled: _isFilled,
},
ref,
) => {
const labelEl = label ? (
formHeader ? (
<div className="flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative shrink-0 w-full">
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
<label
id={labelId}
htmlFor={inputId}
className={`${labelClasses} font-medium text-[var(--color-content-default-primary)]`}
>
{label}
</label>
{showHelpIcon && (
<div className="relative shrink-0 size-[12px]">
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt={helpIconAlt}
className="block max-w-none size-full"
/>
</div>
)}
</div>
</div>
) : (
<label id={labelId} htmlFor={inputId} className="sr-only">
{label}
</label>
)
) : null;
return (
<div className={containerClasses}>
{formHeader && label && (
<div className="flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative shrink-0 w-full">
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
<label
id={labelId}
htmlFor={inputId}
className={`${labelClasses} font-medium text-[var(--color-content-default-primary)]`}
>
{label}
</label>
{showHelpIcon && (
<div className="relative shrink-0 size-[12px]">
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt={helpIconAlt}
className="block max-w-none size-full"
/>
</div>
)}
</div>
</div>
)}
{labelEl}
<div className={inputWrapperClasses}>
<div className={disabled ? "opacity-40" : ""}>
<input
@@ -71,9 +86,16 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={onKeyDown}
onMouseDown={handleMouseDown}
disabled={disabled}
maxLength={maxLength}
autoComplete={autoComplete}
inputMode={inputMode}
required={required}
form={form}
aria-invalid={error || undefined}
aria-required={required || undefined}
className={inputClasses}
style={{ borderRadius }}
/>