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:
@@ -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 }}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user