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,104 +227,114 @@ 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" && (
<RadioGroup <div className="space-y-1">
value={formState.type} <Label>Tipo de anotação</Label>
onValueChange={(value) => <RadioGroup
updateField("type", value as "nota" | "tarefa") value={formState.type}
} onValueChange={(value) =>
disabled={isPending} updateField("type", value as "nota" | "tarefa")
className="flex gap-4" }
> disabled={isPending}
<div className="flex items-center gap-2"> className="flex gap-4"
<RadioGroupItem value="nota" id="tipo-nota" /> >
<label <div className="flex items-center gap-2">
htmlFor="tipo-nota" <RadioGroupItem value="nota" id="tipo-nota" />
className="text-sm cursor-pointer select-none" <Label
> htmlFor="tipo-nota"
Nota className="font-normal cursor-pointer"
</label> >
</div> Nota
<div className="flex items-center gap-2"> </Label>
<RadioGroupItem value="tarefa" id="tipo-tarefa" /> </div>
<label <div className="flex items-center gap-2">
htmlFor="tipo-tarefa" <RadioGroupItem value="tarefa" id="tipo-tarefa" />
className="text-sm cursor-pointer select-none" <Label
> htmlFor="tipo-tarefa"
Tarefas className="font-normal cursor-pointer"
</label> >
</div> Tarefas
</RadioGroup> </Label>
</div>
</RadioGroup>
</div>
)} )}
<Input <div className="space-y-1">
id="note-title" <Label htmlFor="note-title">Título</Label>
ref={titleRef} <Input
value={formState.title} id="note-title"
onChange={(e) => updateField("title", e.target.value)} ref={titleRef}
placeholder="Título" value={formState.title}
maxLength={MAX_TITLE} onChange={(e) => updateField("title", e.target.value)}
disabled={isPending} placeholder={
required isNote ? "Ex.: Revisar metas do mês" : "Ex.: Tarefas da semana"
/> }
maxLength={MAX_TITLE}
{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}
disabled={isPending} disabled={isPending}
required 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 && ( {!isNote && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex gap-2"> <div className="space-y-1">
<Input <Label htmlFor="new-task-input">Adicionar tarefa</Label>
id="new-task-input" <div className="flex gap-2">
ref={newTaskRef} <Input
value={newTaskText} id="new-task-input"
onChange={(e) => setNewTaskText(e.target.value)} ref={newTaskRef}
placeholder="Nova tarefa..." value={newTaskText}
disabled={isPending} onChange={(e) => setNewTaskText(e.target.value)}
onKeyDown={(e) => { placeholder="Nova tarefa..."
if (e.key === "Enter") { disabled={isPending}
e.preventDefault(); onKeyDown={(e) => {
handleAddTask(); if (e.key === "Enter") {
} e.preventDefault();
}} handleAddTask();
/> }
<Button }}
type="button" />
variant="outline" <Button
onClick={handleAddTask} type="button"
disabled={isPending || !normalize(newTaskText)} variant="outline"
className="shrink-0" onClick={handleAddTask}
> disabled={isPending || !normalize(newTaskText)}
<RiAddLine className="h-4 w-4" /> className="shrink-0"
</Button> >
<RiAddLine className="h-4 w-4" />
</Button>
</div>
</div> </div>
{sortedTasks.length > 0 && ( {sortedTasks.length > 0 && (
@@ -370,21 +383,33 @@ export function NoteDialog({
{errorMessage} {errorMessage}
</p> </p>
) : null} ) : 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> </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> </DialogContent>
</Dialog> </Dialog>
); );