style: padronizar note-dialog com Labels e scroll apenas no conteúdo

Adiciona Labels (Título, Conteúdo, Tipo de anotação, Adicionar tarefa)
seguindo o padrão dos demais dialogs do projeto (space-y-1 + Label).

DialogDescription visível novamente com texto contextual.

Scroll apenas no form (-mx-6 max-h-[80vh] overflow-y-auto px-6),
header e footer fixos — mesmo padrão do lancamento-dialog.

Footer movido para fora do form; submit via requestSubmit().

Corrige useMemo antes do early return no note-details-dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-26 18:04:41 +00:00
parent 81cc99cd91
commit f0b8758cc2
2 changed files with 125 additions and 99 deletions

View File

@@ -45,13 +45,14 @@ export function NoteDetailsDialog({
}; };
}, [note]); }, [note]);
const tasks = note?.tasks || [];
const sortedTasks = useMemo(() => sortTasksByStatus(tasks), [tasks]);
if (!note) { if (!note) {
return null; return null;
} }
const isTask = note.type === "tarefa"; const isTask = note.type === "tarefa";
const tasks = note.tasks || [];
const sortedTasks = useMemo(() => sortTasksByStatus(tasks), [tasks]);
const completedCount = tasks.filter((t) => t.completed).length; const completedCount = tasks.filter((t) => t.completed).length;
const totalCount = tasks.length; const totalCount = tasks.length;

View File

@@ -26,6 +26,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { useControlledState } from "@/hooks/use-controlled-state"; import { useControlledState } from "@/hooks/use-controlled-state";
@@ -76,7 +77,6 @@ export function NoteDialog({
const descRef = useRef<HTMLTextAreaElement>(null); const descRef = useRef<HTMLTextAreaElement>(null);
const newTaskRef = useRef<HTMLInputElement>(null); const newTaskRef = useRef<HTMLInputElement>(null);
// Use controlled state hook for dialog open state
const [dialogOpen, setDialogOpen] = useControlledState( const [dialogOpen, setDialogOpen] = useControlledState(
open, open,
false, false,
@@ -85,7 +85,6 @@ export function NoteDialog({
const initialState = buildInitialValues(note); const initialState = buildInitialValues(note);
// Use form state hook for form management
const { formState, updateField, setFormState } = const { formState, updateField, setFormState } =
useFormState<NoteFormValues>(initialState); useFormState<NoteFormValues>(initialState);
@@ -98,7 +97,11 @@ export function NoteDialog({
} }
}, [dialogOpen, note, setFormState]); }, [dialogOpen, note, setFormState]);
const title = mode === "create" ? "Nova anotação" : "Editar anotação"; const dialogTitle = mode === "create" ? "Nova anotação" : "Editar anotação";
const description =
mode === "create"
? "Crie uma nota simples ou uma lista de tarefas."
: "Altere o conteúdo desta anotação.";
const submitLabel = mode === "create" ? "Salvar" : "Atualizar"; const submitLabel = mode === "create" ? "Salvar" : "Atualizar";
const titleCount = formState.title.length; const titleCount = formState.title.length;
@@ -224,23 +227,21 @@ export function NoteDialog({
return ( return (
<Dialog open={dialogOpen} onOpenChange={handleOpenChange}> <Dialog open={dialogOpen} onOpenChange={handleOpenChange}>
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null} {trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
<DialogContent className="max-h-[90vh] overflow-y-auto"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{dialogTitle}</DialogTitle>
<DialogDescription className="sr-only"> <DialogDescription>{description}</DialogDescription>
{mode === "create"
? "Criar nova anotação"
: "Editar anotação existente"}
</DialogDescription>
</DialogHeader> </DialogHeader>
<form <form
className="space-y-3" className="space-y-3 -mx-6 max-h-[80vh] overflow-y-auto px-6 pb-1"
onSubmit={handleSubmit} onSubmit={handleSubmit}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
noValidate noValidate
> >
{mode === "create" && ( {mode === "create" && (
<div className="space-y-1">
<Label>Tipo de anotação</Label>
<RadioGroup <RadioGroup
value={formState.type} value={formState.type}
onValueChange={(value) => onValueChange={(value) =>
@@ -251,53 +252,64 @@ export function NoteDialog({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<RadioGroupItem value="nota" id="tipo-nota" /> <RadioGroupItem value="nota" id="tipo-nota" />
<label <Label
htmlFor="tipo-nota" htmlFor="tipo-nota"
className="text-sm cursor-pointer select-none" className="font-normal cursor-pointer"
> >
Nota Nota
</label> </Label>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<RadioGroupItem value="tarefa" id="tipo-tarefa" /> <RadioGroupItem value="tarefa" id="tipo-tarefa" />
<label <Label
htmlFor="tipo-tarefa" htmlFor="tipo-tarefa"
className="text-sm cursor-pointer select-none" className="font-normal cursor-pointer"
> >
Tarefas Tarefas
</label> </Label>
</div> </div>
</RadioGroup> </RadioGroup>
</div>
)} )}
<div className="space-y-1">
<Label htmlFor="note-title">Título</Label>
<Input <Input
id="note-title" id="note-title"
ref={titleRef} ref={titleRef}
value={formState.title} value={formState.title}
onChange={(e) => updateField("title", e.target.value)} onChange={(e) => updateField("title", e.target.value)}
placeholder="Título" placeholder={
isNote ? "Ex.: Revisar metas do mês" : "Ex.: Tarefas da semana"
}
maxLength={MAX_TITLE} maxLength={MAX_TITLE}
disabled={isPending} disabled={isPending}
required required
/> />
</div>
{isNote && ( {isNote && (
<div className="space-y-1">
<Label htmlFor="note-description">Conteúdo</Label>
<Textarea <Textarea
id="note-description" id="note-description"
className="field-sizing-fixed" className="field-sizing-fixed"
ref={descRef} ref={descRef}
value={formState.description} value={formState.description}
onChange={(e) => updateField("description", e.target.value)} onChange={(e) => updateField("description", e.target.value)}
placeholder="Escreva sua anotação..." placeholder="Detalhe sua anotação..."
rows={5} rows={5}
maxLength={MAX_DESC} maxLength={MAX_DESC}
disabled={isPending} disabled={isPending}
required required
/> />
</div>
)} )}
{!isNote && ( {!isNote && (
<div className="space-y-2"> <div className="space-y-2">
<div className="space-y-1">
<Label htmlFor="new-task-input">Adicionar tarefa</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
id="new-task-input" id="new-task-input"
@@ -323,6 +335,7 @@ export function NoteDialog({
<RiAddLine className="h-4 w-4" /> <RiAddLine className="h-4 w-4" />
</Button> </Button>
</div> </div>
</div>
{sortedTasks.length > 0 && ( {sortedTasks.length > 0 && (
<div className="space-y-1 max-h-[240px] overflow-y-auto pr-1"> <div className="space-y-1 max-h-[240px] overflow-y-auto pr-1">
@@ -370,6 +383,7 @@ export function NoteDialog({
{errorMessage} {errorMessage}
</p> </p>
) : null} ) : null}
</form>
<DialogFooter> <DialogFooter>
<Button <Button
@@ -380,11 +394,22 @@ export function NoteDialog({
> >
Cancelar Cancelar
</Button> </Button>
<Button type="submit" disabled={disableSubmit}> <Button
type="submit"
disabled={disableSubmit}
onClick={(e) => {
const form = (
e.currentTarget.closest("[role=dialog]") as HTMLElement
)?.querySelector("form");
if (form) {
e.preventDefault();
form.requestSubmit();
}
}}
>
{isPending ? "Salvando..." : submitLabel} {isPending ? "Salvando..." : submitLabel}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );