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]);
const tasks = note?.tasks || [];
const sortedTasks = useMemo(() => sortTasksByStatus(tasks), [tasks]);
if (!note) {
return null;
}
const isTask = note.type === "tarefa";
const tasks = note.tasks || [];
const sortedTasks = useMemo(() => sortTasksByStatus(tasks), [tasks]);
const completedCount = tasks.filter((t) => t.completed).length;
const totalCount = tasks.length;

View File

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