Form
Organism
Form é uma superfície de coleta e revisão antes de uma ação rastreável.
Formulários no Pulso precisam ser explícitos sobre obrigatoriedade, erro, consequência e estado de envio. Placeholder não substitui label, e mensagens críticas não devem ficar abaixo da dobra sem sinalização.
Exemplos copiáveis
React
React
import * as React from "react"
type CrisisFormValues = {
title: string
source: string
risk: "stable" | "monitor" | "attention" | "high" | "critical"
summary: string
}
type PulsoCrisisFormProps = {
initialValues?: Partial<CrisisFormValues>
onSubmit: (values: CrisisFormValues) => Promise<void> | void
}
const defaultValues: CrisisFormValues = { title: "", source: "", risk: "monitor", summary: "" }
export function PulsoCrisisForm({ initialValues, onSubmit }: PulsoCrisisFormProps) {
const [values, setValues] = React.useState<CrisisFormValues>({ ...defaultValues, ...initialValues })
const [errors, setErrors] = React.useState<Partial<Record<keyof CrisisFormValues, string>>>({})
const [isSubmitting, setIsSubmitting] = React.useState(false)
function updateValue<Key extends keyof CrisisFormValues>(key: Key, value: CrisisFormValues[Key]) {
setValues((current) => ({ ...current, [key]: value }))
setErrors((current) => ({ ...current, [key]: undefined }))
}
function validate(nextValues: CrisisFormValues) {
const nextErrors: Partial<Record<keyof CrisisFormValues, string>> = {}
if (!nextValues.title.trim()) nextErrors.title = "Nomeie o caso antes de continuar."
if (!nextValues.source.trim()) nextErrors.source = "Informe a fonte para manter rastreabilidade."
if (nextValues.summary.trim().length < 24) nextErrors.summary = "Descreva contexto suficiente para revisão humana."
return nextErrors
}
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const nextErrors = validate(values)
setErrors(nextErrors)
if (Object.keys(nextErrors).length > 0) return
setIsSubmitting(true)
try { await onSubmit(values) } finally { setIsSubmitting(false) }
}
return (
<form onSubmit={handleSubmit} noValidate className="grid gap-5">
<div className="grid gap-1.5">
<label htmlFor="case-title" className="font-mono text-xs uppercase tracking-[var(--tracking-kicker)] text-[var(--fg-muted)]">Nome do caso</label>
<input id="case-title" value={values.title} onChange={(event) => updateValue("title", event.target.value)} aria-invalid={Boolean(errors.title)} aria-describedby={errors.title ? "case-title-error" : undefined} className="min-h-10 rounded-[var(--radius-md)] border border-[var(--border)] bg-[var(--card)] px-3 text-sm" />
{errors.title ? <p id="case-title-error" className="text-sm text-[var(--risk-critical-text)]">{errors.title}</p> : null}
</div>
<div className="grid gap-1.5">
<label htmlFor="case-risk" className="font-mono text-xs uppercase tracking-[var(--tracking-kicker)] text-[var(--fg-muted)]">Risco operacional</label>
<select id="case-risk" value={values.risk} onChange={(event) => updateValue("risk", event.target.value as CrisisFormValues["risk"])} className="min-h-10 rounded-[var(--radius-md)] border border-[var(--border)] bg-[var(--card)] px-3 text-sm">
<option value="stable">stable</option><option value="monitor">monitor</option><option value="attention">attention</option><option value="high">high</option><option value="critical">critical</option>
</select>
</div>
<div className="grid gap-1.5">
<label htmlFor="case-summary" className="font-mono text-xs uppercase tracking-[var(--tracking-kicker)] text-[var(--fg-muted)]">Contexto para decisão</label>
<textarea id="case-summary" value={values.summary} onChange={(event) => updateValue("summary", event.target.value)} aria-invalid={Boolean(errors.summary)} aria-describedby={errors.summary ? "case-summary-error" : undefined} rows={4} className="rounded-[var(--radius-md)] border border-[var(--border)] bg-[var(--card)] px-3 py-2 text-sm" />
{errors.summary ? <p id="case-summary-error" className="text-sm text-[var(--risk-critical-text)]">{errors.summary}</p> : null}
</div>
<footer className="flex justify-end gap-2 border-t border-[var(--border)] pt-4">
<button type="button" className="rounded-[var(--radius-md)] border border-[var(--border)] px-4 py-2 text-sm">Cancelar</button>
<button type="submit" disabled={isSubmitting} className="rounded-[var(--radius-md)] bg-[var(--primary)] px-4 py-2 text-sm font-semibold text-[var(--primary-fg)]">{isSubmitting ? "Salvando…" : "Salvar caso"}</button>
</footer>
</form>
)
}Regras
- Todo campo precisa de label programático.
- Erros devem ser próximos, específicos e conectados por
aria-describedby. - Risco usa
stable / monitor / attention / high / critical. - Estado de envio precisa bloquear duplicidade e manter feedback claro.
- Ações destrutivas exigem confirmação fora do form comum.
Last updated on