feat(calculadora): adicionar dialog arrastável e seleção de valor

- Calculadora agora é arrastável via drag handle no header
- Novo callback onSelectValue permite inserir valor no campo de lançamento
- Ajustado subtitle de categorias e estilo do collapse na sidebar
- Atualizado snapshot drizzle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-06 13:22:31 +00:00
parent 4152a27f4d
commit 5bb5693baf
11 changed files with 2631 additions and 2510 deletions

View File

@@ -16,6 +16,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useDraggableDialog } from "@/hooks/use-draggable-dialog";
import { cn } from "@/lib/utils/ui";
type Variant = React.ComponentProps<typeof Button>["variant"];
@@ -27,18 +28,62 @@ type CalculatorDialogButtonProps = {
className?: string;
children?: React.ReactNode;
withTooltip?: boolean;
onSelectValue?: (value: string) => void;
};
function CalculatorDialogContent({
open,
onSelectValue,
}: {
open: boolean;
onSelectValue?: (value: string) => void;
}) {
const { dragHandleProps, contentRefCallback, resetPosition } =
useDraggableDialog();
React.useEffect(() => {
if (!open) {
resetPosition();
}
}, [open, resetPosition]);
return (
<DialogContent
ref={contentRefCallback}
className="p-4 sm:max-w-sm"
onEscapeKeyDown={(e) => e.preventDefault()}
>
<DialogHeader
className="cursor-grab select-none space-y-2 active:cursor-grabbing"
{...dragHandleProps}
>
<DialogTitle className="flex items-center gap-2 text-lg">
<RiCalculatorLine className="h-5 w-5" />
Calculadora
</DialogTitle>
</DialogHeader>
<Calculator isOpen={open} onSelectValue={onSelectValue} />
</DialogContent>
);
}
export function CalculatorDialogButton({
variant = "ghost",
size = "sm",
className,
children,
withTooltip = false,
onSelectValue,
}: CalculatorDialogButtonProps) {
const [open, setOpen] = React.useState(false);
// Se withTooltip for true, usa o estilo do header
const handleSelectValue = onSelectValue
? (value: string) => {
onSelectValue(value);
setOpen(false);
}
: undefined;
if (withTooltip) {
return (
<Dialog open={open} onOpenChange={setOpen}>
@@ -72,20 +117,14 @@ export function CalculatorDialogButton({
Calculadora
</TooltipContent>
</Tooltip>
<DialogContent className="p-4 sm:max-w-sm">
<DialogHeader className="space-y-2">
<DialogTitle className="flex items-center gap-2 text-lg">
<RiCalculatorLine className="h-5 w-5" />
Calculadora
</DialogTitle>
</DialogHeader>
<Calculator />
</DialogContent>
<CalculatorDialogContent
open={open}
onSelectValue={handleSelectValue}
/>
</Dialog>
);
}
// Estilo padrão para outros usos
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@@ -95,15 +134,7 @@ export function CalculatorDialogButton({
)}
</Button>
</DialogTrigger>
<DialogContent className="p-4 sm:max-w-sm">
<DialogHeader className="space-y-2">
<DialogTitle className="flex items-center gap-2 text-lg">
<RiCalculatorLine className="h-5 w-5" />
Calculadora
</DialogTitle>
</DialogHeader>
<Calculator />
</DialogContent>
<CalculatorDialogContent open={open} onSelectValue={handleSelectValue} />
</Dialog>
);
}

View File

@@ -1,29 +1,49 @@
import { Button } from "@/components/ui/button";
import type { CalculatorButtonConfig } from "@/hooks/use-calculator-state";
import type { Operator } from "@/lib/utils/calculator";
import { cn } from "@/lib/utils/ui";
type CalculatorKeypadProps = {
buttons: CalculatorButtonConfig[][];
activeOperator: Operator | null;
};
export function CalculatorKeypad({ buttons }: CalculatorKeypadProps) {
const LABEL_TO_OPERATOR: Record<string, Operator> = {
"÷": "divide",
"×": "multiply",
"-": "subtract",
"+": "add",
};
export function CalculatorKeypad({
buttons,
activeOperator,
}: CalculatorKeypadProps) {
return (
<div className="grid grid-cols-4 gap-2">
{buttons.flat().map((btn, index) => (
<Button
key={`${btn.label}-${index}`}
type="button"
variant={btn.variant ?? "outline"}
onClick={btn.onClick}
className={cn(
"h-12 text-base font-semibold",
btn.colSpan === 2 && "col-span-2",
btn.colSpan === 3 && "col-span-3",
)}
>
{btn.label}
</Button>
))}
{buttons.flat().map((btn, index) => {
const op = LABEL_TO_OPERATOR[btn.label];
const isActive = op != null && op === activeOperator;
return (
<Button
key={`${btn.label}-${index}`}
type="button"
variant={isActive ? "default" : (btn.variant ?? "outline")}
onClick={btn.onClick}
className={cn(
"h-12 text-base font-semibold",
btn.colSpan === 2 && "col-span-2",
btn.colSpan === 3 && "col-span-3",
isActive &&
"bg-primary text-primary-foreground hover:bg-primary/90 ring-2 ring-primary/30",
btn.className,
)}
>
{btn.label}
</Button>
);
})}
</div>
);
}

View File

@@ -1,27 +1,61 @@
"use client";
import { CalculatorKeypad } from "@/components/calculadora/calculator-keypad";
import { Button } from "@/components/ui/button";
import { useCalculatorKeyboard } from "@/hooks/use-calculator-keyboard";
import { useCalculatorState } from "@/hooks/use-calculator-state";
import { CalculatorDisplay } from "./calculator-display";
export default function Calculator() {
type CalculatorProps = {
isOpen?: boolean;
onSelectValue?: (value: string) => void;
};
export default function Calculator({
isOpen = true,
onSelectValue,
}: CalculatorProps) {
const {
display,
operator,
expression,
history,
resultText,
copied,
buttons,
inputDigit,
inputDecimal,
setNextOperator,
evaluate,
deleteLastDigit,
reset,
applyPercent,
copyToClipboard,
pasteFromClipboard,
} = useCalculatorState();
useCalculatorKeyboard({
isOpen,
canCopy: Boolean(resultText),
onCopy: copyToClipboard,
onPaste: pasteFromClipboard,
inputDigit,
inputDecimal,
setNextOperator,
evaluate,
deleteLastDigit,
reset,
applyPercent,
});
const canUseValue = onSelectValue && display !== "Erro" && display !== "0";
const handleSelectValue = () => {
if (!onSelectValue) return;
const numericValue = Math.abs(Number(display)).toFixed(2);
onSelectValue(numericValue);
};
return (
<div className="space-y-4">
<CalculatorDisplay
@@ -31,7 +65,18 @@ export default function Calculator() {
copied={copied}
onCopy={copyToClipboard}
/>
<CalculatorKeypad buttons={buttons} />
<CalculatorKeypad buttons={buttons} activeOperator={operator} />
{onSelectValue && (
<Button
type="button"
variant="default"
className="w-full"
disabled={!canUseValue}
onClick={handleSelectValue}
>
Usar valor
</Button>
)}
</div>
);
}

View File

@@ -67,6 +67,7 @@ export function BasicFieldsSection({
variant="ghost"
size="icon-sm"
className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2"
onSelectValue={(value) => onFieldChange("amount", value)}
>
<RiCalculatorLine className="h-4 w-4 text-muted-foreground" />
</CalculatorDialogButton>

View File

@@ -7,7 +7,6 @@ import {
} from "@remixicon/react";
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
@@ -134,8 +133,8 @@ export function NavMain({ sections }: { sections: NavSection[] }) {
{item.items?.length ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90 text-foreground px-2 trasition-transform duration-200">
<RiArrowRightSLine />
<SidebarMenuAction className="data-[state=open]:rotate-90 px-2 trasition-transform duration-200">
<RiArrowRightSLine className="text-primary" />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>