forked from git.gladyson/openmonetis
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:
@@ -15,7 +15,7 @@ export default function RootLayout({
|
|||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiPriceTag3Line />}
|
icon={<RiPriceTag3Line />}
|
||||||
title="Categorias"
|
title="Categorias"
|
||||||
subtitle="Gerencie suas categorias de despesas e receitas acompanhando o histórico de desempenho dos últimos 9 meses, permitindo ajustes financeiros precisos conforme necessário."
|
subtitle="Gerencie suas categorias de despesas e receitas, permitindo ajustes financeiros precisos conforme necessário."
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { useDraggableDialog } from "@/hooks/use-draggable-dialog";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
|
||||||
type Variant = React.ComponentProps<typeof Button>["variant"];
|
type Variant = React.ComponentProps<typeof Button>["variant"];
|
||||||
@@ -27,18 +28,62 @@ type CalculatorDialogButtonProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
withTooltip?: boolean;
|
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({
|
export function CalculatorDialogButton({
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
size = "sm",
|
size = "sm",
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
withTooltip = false,
|
withTooltip = false,
|
||||||
|
onSelectValue,
|
||||||
}: CalculatorDialogButtonProps) {
|
}: CalculatorDialogButtonProps) {
|
||||||
const [open, setOpen] = React.useState(false);
|
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) {
|
if (withTooltip) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -72,20 +117,14 @@ export function CalculatorDialogButton({
|
|||||||
Calculadora
|
Calculadora
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DialogContent className="p-4 sm:max-w-sm">
|
<CalculatorDialogContent
|
||||||
<DialogHeader className="space-y-2">
|
open={open}
|
||||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
onSelectValue={handleSelectValue}
|
||||||
<RiCalculatorLine className="h-5 w-5" />
|
/>
|
||||||
Calculadora
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<Calculator />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estilo padrão para outros usos
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -95,15 +134,7 @@ export function CalculatorDialogButton({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="p-4 sm:max-w-sm">
|
<CalculatorDialogContent open={open} onSelectValue={handleSelectValue} />
|
||||||
<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>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,49 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { CalculatorButtonConfig } from "@/hooks/use-calculator-state";
|
import type { CalculatorButtonConfig } from "@/hooks/use-calculator-state";
|
||||||
|
import type { Operator } from "@/lib/utils/calculator";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
|
||||||
type CalculatorKeypadProps = {
|
type CalculatorKeypadProps = {
|
||||||
buttons: CalculatorButtonConfig[][];
|
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 (
|
return (
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
{buttons.flat().map((btn, index) => (
|
{buttons.flat().map((btn, index) => {
|
||||||
|
const op = LABEL_TO_OPERATOR[btn.label];
|
||||||
|
const isActive = op != null && op === activeOperator;
|
||||||
|
|
||||||
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={`${btn.label}-${index}`}
|
key={`${btn.label}-${index}`}
|
||||||
type="button"
|
type="button"
|
||||||
variant={btn.variant ?? "outline"}
|
variant={isActive ? "default" : (btn.variant ?? "outline")}
|
||||||
onClick={btn.onClick}
|
onClick={btn.onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-12 text-base font-semibold",
|
"h-12 text-base font-semibold",
|
||||||
btn.colSpan === 2 && "col-span-2",
|
btn.colSpan === 2 && "col-span-2",
|
||||||
btn.colSpan === 3 && "col-span-3",
|
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}
|
{btn.label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,61 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { CalculatorKeypad } from "@/components/calculadora/calculator-keypad";
|
import { CalculatorKeypad } from "@/components/calculadora/calculator-keypad";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useCalculatorKeyboard } from "@/hooks/use-calculator-keyboard";
|
import { useCalculatorKeyboard } from "@/hooks/use-calculator-keyboard";
|
||||||
import { useCalculatorState } from "@/hooks/use-calculator-state";
|
import { useCalculatorState } from "@/hooks/use-calculator-state";
|
||||||
import { CalculatorDisplay } from "./calculator-display";
|
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 {
|
const {
|
||||||
|
display,
|
||||||
|
operator,
|
||||||
expression,
|
expression,
|
||||||
history,
|
history,
|
||||||
resultText,
|
resultText,
|
||||||
copied,
|
copied,
|
||||||
buttons,
|
buttons,
|
||||||
|
inputDigit,
|
||||||
|
inputDecimal,
|
||||||
|
setNextOperator,
|
||||||
|
evaluate,
|
||||||
|
deleteLastDigit,
|
||||||
|
reset,
|
||||||
|
applyPercent,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
pasteFromClipboard,
|
pasteFromClipboard,
|
||||||
} = useCalculatorState();
|
} = useCalculatorState();
|
||||||
|
|
||||||
useCalculatorKeyboard({
|
useCalculatorKeyboard({
|
||||||
|
isOpen,
|
||||||
canCopy: Boolean(resultText),
|
canCopy: Boolean(resultText),
|
||||||
onCopy: copyToClipboard,
|
onCopy: copyToClipboard,
|
||||||
onPaste: pasteFromClipboard,
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<CalculatorDisplay
|
<CalculatorDisplay
|
||||||
@@ -31,7 +65,18 @@ export default function Calculator() {
|
|||||||
copied={copied}
|
copied={copied}
|
||||||
onCopy={copyToClipboard}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export function BasicFieldsSection({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2"
|
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" />
|
<RiCalculatorLine className="h-4 w-4 text-muted-foreground" />
|
||||||
</CalculatorDialogButton>
|
</CalculatorDialogButton>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useSearchParams } from "next/navigation";
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
import * as React from "react";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -134,8 +133,8 @@ export function NavMain({ sections }: { sections: NavSection[] }) {
|
|||||||
{item.items?.length ? (
|
{item.items?.length ? (
|
||||||
<>
|
<>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuAction className="data-[state=open]:rotate-90 text-foreground px-2 trasition-transform duration-200">
|
<SidebarMenuAction className="data-[state=open]:rotate-90 px-2 trasition-transform duration-200">
|
||||||
<RiArrowRightSLine />
|
<RiArrowRightSLine className="text-primary" />
|
||||||
<span className="sr-only">Toggle</span>
|
<span className="sr-only">Toggle</span>
|
||||||
</SidebarMenuAction>
|
</SidebarMenuAction>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
|||||||
@@ -93,12 +93,8 @@
|
|||||||
"name": "account_userId_user_id_fk",
|
"name": "account_userId_user_id_fk",
|
||||||
"tableFrom": "account",
|
"tableFrom": "account",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["userId"],
|
||||||
"userId"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -172,12 +168,8 @@
|
|||||||
"name": "anotacoes_user_id_user_id_fk",
|
"name": "anotacoes_user_id_user_id_fk",
|
||||||
"tableFrom": "anotacoes",
|
"tableFrom": "anotacoes",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -317,12 +309,8 @@
|
|||||||
"name": "antecipacoes_parcelas_lancamento_id_lancamentos_id_fk",
|
"name": "antecipacoes_parcelas_lancamento_id_lancamentos_id_fk",
|
||||||
"tableFrom": "antecipacoes_parcelas",
|
"tableFrom": "antecipacoes_parcelas",
|
||||||
"tableTo": "lancamentos",
|
"tableTo": "lancamentos",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["lancamento_id"],
|
||||||
"lancamento_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -330,12 +318,8 @@
|
|||||||
"name": "antecipacoes_parcelas_pagador_id_pagadores_id_fk",
|
"name": "antecipacoes_parcelas_pagador_id_pagadores_id_fk",
|
||||||
"tableFrom": "antecipacoes_parcelas",
|
"tableFrom": "antecipacoes_parcelas",
|
||||||
"tableTo": "pagadores",
|
"tableTo": "pagadores",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["pagador_id"],
|
||||||
"pagador_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -343,12 +327,8 @@
|
|||||||
"name": "antecipacoes_parcelas_categoria_id_categorias_id_fk",
|
"name": "antecipacoes_parcelas_categoria_id_categorias_id_fk",
|
||||||
"tableFrom": "antecipacoes_parcelas",
|
"tableFrom": "antecipacoes_parcelas",
|
||||||
"tableTo": "categorias",
|
"tableTo": "categorias",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["categoria_id"],
|
||||||
"categoria_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -356,12 +336,8 @@
|
|||||||
"name": "antecipacoes_parcelas_user_id_user_id_fk",
|
"name": "antecipacoes_parcelas_user_id_user_id_fk",
|
||||||
"tableFrom": "antecipacoes_parcelas",
|
"tableFrom": "antecipacoes_parcelas",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -479,12 +455,8 @@
|
|||||||
"name": "cartoes_user_id_user_id_fk",
|
"name": "cartoes_user_id_user_id_fk",
|
||||||
"tableFrom": "cartoes",
|
"tableFrom": "cartoes",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -492,12 +464,8 @@
|
|||||||
"name": "cartoes_conta_id_contas_id_fk",
|
"name": "cartoes_conta_id_contas_id_fk",
|
||||||
"tableFrom": "cartoes",
|
"tableFrom": "cartoes",
|
||||||
"tableTo": "contas",
|
"tableTo": "contas",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["conta_id"],
|
||||||
"conta_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
}
|
}
|
||||||
@@ -579,12 +547,8 @@
|
|||||||
"name": "categorias_user_id_user_id_fk",
|
"name": "categorias_user_id_user_id_fk",
|
||||||
"tableFrom": "categorias",
|
"tableFrom": "categorias",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -667,12 +631,8 @@
|
|||||||
"name": "compartilhamentos_pagador_pagador_id_pagadores_id_fk",
|
"name": "compartilhamentos_pagador_pagador_id_pagadores_id_fk",
|
||||||
"tableFrom": "compartilhamentos_pagador",
|
"tableFrom": "compartilhamentos_pagador",
|
||||||
"tableTo": "pagadores",
|
"tableTo": "pagadores",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["pagador_id"],
|
||||||
"pagador_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -680,12 +640,8 @@
|
|||||||
"name": "compartilhamentos_pagador_shared_with_user_id_user_id_fk",
|
"name": "compartilhamentos_pagador_shared_with_user_id_user_id_fk",
|
||||||
"tableFrom": "compartilhamentos_pagador",
|
"tableFrom": "compartilhamentos_pagador",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["shared_with_user_id"],
|
||||||
"shared_with_user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -693,12 +649,8 @@
|
|||||||
"name": "compartilhamentos_pagador_created_by_user_id_user_id_fk",
|
"name": "compartilhamentos_pagador_created_by_user_id_user_id_fk",
|
||||||
"tableFrom": "compartilhamentos_pagador",
|
"tableFrom": "compartilhamentos_pagador",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["created_by_user_id"],
|
||||||
"created_by_user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -813,12 +765,8 @@
|
|||||||
"name": "contas_user_id_user_id_fk",
|
"name": "contas_user_id_user_id_fk",
|
||||||
"tableFrom": "contas",
|
"tableFrom": "contas",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -921,12 +869,8 @@
|
|||||||
"name": "faturas_user_id_user_id_fk",
|
"name": "faturas_user_id_user_id_fk",
|
||||||
"tableFrom": "faturas",
|
"tableFrom": "faturas",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -934,12 +878,8 @@
|
|||||||
"name": "faturas_cartao_id_cartoes_id_fk",
|
"name": "faturas_cartao_id_cartoes_id_fk",
|
||||||
"tableFrom": "faturas",
|
"tableFrom": "faturas",
|
||||||
"tableTo": "cartoes",
|
"tableTo": "cartoes",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["cartao_id"],
|
||||||
"cartao_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
}
|
}
|
||||||
@@ -1028,12 +968,8 @@
|
|||||||
"name": "insights_salvos_user_id_user_id_fk",
|
"name": "insights_salvos_user_id_user_id_fk",
|
||||||
"tableFrom": "insights_salvos",
|
"tableFrom": "insights_salvos",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1379,12 +1315,8 @@
|
|||||||
"name": "lancamentos_antecipacao_id_antecipacoes_parcelas_id_fk",
|
"name": "lancamentos_antecipacao_id_antecipacoes_parcelas_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "antecipacoes_parcelas",
|
"tableTo": "antecipacoes_parcelas",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["antecipacao_id"],
|
||||||
"antecipacao_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "set null",
|
"onDelete": "set null",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -1392,12 +1324,8 @@
|
|||||||
"name": "lancamentos_user_id_user_id_fk",
|
"name": "lancamentos_user_id_user_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -1405,12 +1333,8 @@
|
|||||||
"name": "lancamentos_cartao_id_cartoes_id_fk",
|
"name": "lancamentos_cartao_id_cartoes_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "cartoes",
|
"tableTo": "cartoes",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["cartao_id"],
|
||||||
"cartao_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
},
|
},
|
||||||
@@ -1418,12 +1342,8 @@
|
|||||||
"name": "lancamentos_conta_id_contas_id_fk",
|
"name": "lancamentos_conta_id_contas_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "contas",
|
"tableTo": "contas",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["conta_id"],
|
||||||
"conta_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
},
|
},
|
||||||
@@ -1431,12 +1351,8 @@
|
|||||||
"name": "lancamentos_categoria_id_categorias_id_fk",
|
"name": "lancamentos_categoria_id_categorias_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "categorias",
|
"tableTo": "categorias",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["categoria_id"],
|
||||||
"categoria_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
},
|
},
|
||||||
@@ -1444,12 +1360,8 @@
|
|||||||
"name": "lancamentos_pagador_id_pagadores_id_fk",
|
"name": "lancamentos_pagador_id_pagadores_id_fk",
|
||||||
"tableFrom": "lancamentos",
|
"tableFrom": "lancamentos",
|
||||||
"tableTo": "pagadores",
|
"tableTo": "pagadores",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["pagador_id"],
|
||||||
"pagador_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
}
|
}
|
||||||
@@ -1531,12 +1443,8 @@
|
|||||||
"name": "orcamentos_user_id_user_id_fk",
|
"name": "orcamentos_user_id_user_id_fk",
|
||||||
"tableFrom": "orcamentos",
|
"tableFrom": "orcamentos",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -1544,12 +1452,8 @@
|
|||||||
"name": "orcamentos_categoria_id_categorias_id_fk",
|
"name": "orcamentos_categoria_id_categorias_id_fk",
|
||||||
"tableFrom": "orcamentos",
|
"tableFrom": "orcamentos",
|
||||||
"tableTo": "categorias",
|
"tableTo": "categorias",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["categoria_id"],
|
||||||
"categoria_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "cascade"
|
"onUpdate": "cascade"
|
||||||
}
|
}
|
||||||
@@ -1705,12 +1609,8 @@
|
|||||||
"name": "pagadores_user_id_user_id_fk",
|
"name": "pagadores_user_id_user_id_fk",
|
||||||
"tableFrom": "pagadores",
|
"tableFrom": "pagadores",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1869,12 +1769,8 @@
|
|||||||
"name": "pre_lancamentos_user_id_user_id_fk",
|
"name": "pre_lancamentos_user_id_user_id_fk",
|
||||||
"tableFrom": "pre_lancamentos",
|
"tableFrom": "pre_lancamentos",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
@@ -1882,12 +1778,8 @@
|
|||||||
"name": "pre_lancamentos_lancamento_id_lancamentos_id_fk",
|
"name": "pre_lancamentos_lancamento_id_lancamentos_id_fk",
|
||||||
"tableFrom": "pre_lancamentos",
|
"tableFrom": "pre_lancamentos",
|
||||||
"tableTo": "lancamentos",
|
"tableTo": "lancamentos",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["lancamento_id"],
|
||||||
"lancamento_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "set null",
|
"onDelete": "set null",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1949,12 +1841,8 @@
|
|||||||
"name": "preferencias_usuario_user_id_user_id_fk",
|
"name": "preferencias_usuario_user_id_user_id_fk",
|
||||||
"tableFrom": "preferencias_usuario",
|
"tableFrom": "preferencias_usuario",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -1964,9 +1852,7 @@
|
|||||||
"preferencias_usuario_user_id_unique": {
|
"preferencias_usuario_user_id_unique": {
|
||||||
"name": "preferencias_usuario_user_id_unique",
|
"name": "preferencias_usuario_user_id_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["user_id"]
|
||||||
"user_id"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
@@ -2032,12 +1918,8 @@
|
|||||||
"name": "session_userId_user_id_fk",
|
"name": "session_userId_user_id_fk",
|
||||||
"tableFrom": "session",
|
"tableFrom": "session",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["userId"],
|
||||||
"userId"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -2047,9 +1929,7 @@
|
|||||||
"session_token_unique": {
|
"session_token_unique": {
|
||||||
"name": "session_token_unique",
|
"name": "session_token_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["token"]
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
@@ -2160,12 +2040,8 @@
|
|||||||
"name": "tokens_api_user_id_user_id_fk",
|
"name": "tokens_api_user_id_user_id_fk",
|
||||||
"tableFrom": "tokens_api",
|
"tableFrom": "tokens_api",
|
||||||
"tableTo": "user",
|
"tableTo": "user",
|
||||||
"columnsFrom": [
|
"columnsFrom": ["user_id"],
|
||||||
"user_id"
|
"columnsTo": ["id"],
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
@@ -2230,9 +2106,7 @@
|
|||||||
"user_email_unique": {
|
"user_email_unique": {
|
||||||
"name": "user_email_unique",
|
"name": "user_email_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": ["email"]
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import type { Operator } from "@/lib/utils/calculator";
|
||||||
|
|
||||||
type UseCalculatorKeyboardParams = {
|
type UseCalculatorKeyboardParams = {
|
||||||
|
isOpen: boolean;
|
||||||
canCopy: boolean;
|
canCopy: boolean;
|
||||||
onCopy: () => void | Promise<void>;
|
onCopy: () => void | Promise<void>;
|
||||||
onPaste: () => void | Promise<void>;
|
onPaste: () => void | Promise<void>;
|
||||||
|
inputDigit: (digit: string) => void;
|
||||||
|
inputDecimal: () => void;
|
||||||
|
setNextOperator: (op: Operator) => void;
|
||||||
|
evaluate: () => void;
|
||||||
|
deleteLastDigit: () => void;
|
||||||
|
reset: () => void;
|
||||||
|
applyPercent: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function shouldIgnoreForEditableTarget(target: EventTarget | null): boolean {
|
function shouldIgnoreForEditableTarget(target: EventTarget | null): boolean {
|
||||||
@@ -17,76 +26,118 @@ function shouldIgnoreForEditableTarget(target: EventTarget | null): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const KEY_TO_OPERATOR: Record<string, Operator> = {
|
||||||
|
"+": "add",
|
||||||
|
"-": "subtract",
|
||||||
|
"*": "multiply",
|
||||||
|
"/": "divide",
|
||||||
|
};
|
||||||
|
|
||||||
export function useCalculatorKeyboard({
|
export function useCalculatorKeyboard({
|
||||||
|
isOpen,
|
||||||
canCopy,
|
canCopy,
|
||||||
onCopy,
|
onCopy,
|
||||||
onPaste,
|
onPaste,
|
||||||
|
inputDigit,
|
||||||
|
inputDecimal,
|
||||||
|
setNextOperator,
|
||||||
|
evaluate,
|
||||||
|
deleteLastDigit,
|
||||||
|
reset,
|
||||||
|
applyPercent,
|
||||||
}: UseCalculatorKeyboardParams) {
|
}: UseCalculatorKeyboardParams) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canCopy) {
|
if (!isOpen) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (!(event.ctrlKey || event.metaKey)) {
|
const { key, ctrlKey, metaKey } = event;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldIgnoreForEditableTarget(event.target)) {
|
// Ctrl/Cmd shortcuts
|
||||||
return;
|
if (ctrlKey || metaKey) {
|
||||||
}
|
if (shouldIgnoreForEditableTarget(event.target)) return;
|
||||||
|
|
||||||
if (event.key.toLowerCase() !== "c") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const lowerKey = key.toLowerCase();
|
||||||
|
if (lowerKey === "c" && canCopy) {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection && selection.toString().trim().length > 0) {
|
if (selection && selection.toString().trim().length > 0) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void onCopy();
|
void onCopy();
|
||||||
|
} else if (lowerKey === "v") {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection && selection.toString().trim().length > 0) return;
|
||||||
|
if (!navigator.clipboard?.readText) return;
|
||||||
|
event.preventDefault();
|
||||||
|
void onPaste();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Digits
|
||||||
|
if (key >= "0" && key <= "9") {
|
||||||
|
event.preventDefault();
|
||||||
|
inputDigit(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decimal
|
||||||
|
if (key === "." || key === ",") {
|
||||||
|
event.preventDefault();
|
||||||
|
inputDecimal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operators
|
||||||
|
const op = KEY_TO_OPERATOR[key];
|
||||||
|
if (op) {
|
||||||
|
event.preventDefault();
|
||||||
|
setNextOperator(op);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate
|
||||||
|
if (key === "Enter" || key === "=") {
|
||||||
|
event.preventDefault();
|
||||||
|
evaluate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backspace
|
||||||
|
if (key === "Backspace") {
|
||||||
|
event.preventDefault();
|
||||||
|
deleteLastDigit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape resets calculator (dialog close is handled by onEscapeKeyDown)
|
||||||
|
if (key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Percent
|
||||||
|
if (key === "%") {
|
||||||
|
event.preventDefault();
|
||||||
|
applyPercent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", handleKeyDown);
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [canCopy, onCopy]);
|
}, [
|
||||||
|
isOpen,
|
||||||
useEffect(() => {
|
canCopy,
|
||||||
const handlePasteShortcut = (event: KeyboardEvent) => {
|
onCopy,
|
||||||
if (!(event.ctrlKey || event.metaKey)) {
|
onPaste,
|
||||||
return;
|
inputDigit,
|
||||||
}
|
inputDecimal,
|
||||||
|
setNextOperator,
|
||||||
if (event.key.toLowerCase() !== "v") {
|
evaluate,
|
||||||
return;
|
deleteLastDigit,
|
||||||
}
|
reset,
|
||||||
|
applyPercent,
|
||||||
if (shouldIgnoreForEditableTarget(event.target)) {
|
]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection && selection.toString().trim().length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!navigator.clipboard?.readText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
void onPaste();
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("keydown", handlePasteShortcut);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", handlePasteShortcut);
|
|
||||||
};
|
|
||||||
}, [onPaste]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type CalculatorButtonConfig = {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
variant?: VariantProps<typeof buttonVariants>["variant"];
|
variant?: VariantProps<typeof buttonVariants>["variant"];
|
||||||
colSpan?: number;
|
colSpan?: number;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useCalculatorState() {
|
export function useCalculatorState() {
|
||||||
@@ -242,8 +243,8 @@ export function useCalculatorState() {
|
|||||||
const buttons: CalculatorButtonConfig[][] = [
|
const buttons: CalculatorButtonConfig[][] = [
|
||||||
[
|
[
|
||||||
{ label: "C", onClick: reset, variant: "destructive" },
|
{ label: "C", onClick: reset, variant: "destructive" },
|
||||||
{ label: "⌫", onClick: deleteLastDigit, variant: "default" },
|
{ label: "⌫", onClick: deleteLastDigit, variant: "secondary" },
|
||||||
{ label: "%", onClick: applyPercent, variant: "default" },
|
{ label: "%", onClick: applyPercent, variant: "secondary" },
|
||||||
{
|
{
|
||||||
label: "÷",
|
label: "÷",
|
||||||
onClick: makeOperatorHandler("divide"),
|
onClick: makeOperatorHandler("divide"),
|
||||||
@@ -277,7 +278,7 @@ export function useCalculatorState() {
|
|||||||
{ label: "+", onClick: makeOperatorHandler("add"), variant: "outline" },
|
{ label: "+", onClick: makeOperatorHandler("add"), variant: "outline" },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ label: "±", onClick: toggleSign, variant: "default" },
|
{ label: "±", onClick: toggleSign, variant: "secondary" },
|
||||||
{ label: "0", onClick: () => inputDigit("0") },
|
{ label: "0", onClick: () => inputDigit("0") },
|
||||||
{ label: ",", onClick: inputDecimal },
|
{ label: ",", onClick: inputDecimal },
|
||||||
{ label: "=", onClick: evaluate, variant: "default" },
|
{ label: "=", onClick: evaluate, variant: "default" },
|
||||||
@@ -358,11 +359,20 @@ export function useCalculatorState() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
display,
|
||||||
|
operator,
|
||||||
expression,
|
expression,
|
||||||
history,
|
history,
|
||||||
resultText,
|
resultText,
|
||||||
copied,
|
copied,
|
||||||
buttons,
|
buttons,
|
||||||
|
inputDigit,
|
||||||
|
inputDecimal,
|
||||||
|
setNextOperator,
|
||||||
|
evaluate,
|
||||||
|
deleteLastDigit,
|
||||||
|
reset,
|
||||||
|
applyPercent,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
pasteFromClipboard,
|
pasteFromClipboard,
|
||||||
};
|
};
|
||||||
|
|||||||
90
hooks/use-draggable-dialog.ts
Normal file
90
hooks/use-draggable-dialog.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
type Position = { x: number; y: number };
|
||||||
|
|
||||||
|
const MIN_VISIBLE_PX = 20;
|
||||||
|
|
||||||
|
function clampPosition(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
elementWidth: number,
|
||||||
|
elementHeight: number,
|
||||||
|
): Position {
|
||||||
|
const maxX = window.innerWidth - MIN_VISIBLE_PX;
|
||||||
|
const minX = MIN_VISIBLE_PX - elementWidth;
|
||||||
|
const maxY = window.innerHeight - MIN_VISIBLE_PX;
|
||||||
|
const minY = MIN_VISIBLE_PX - elementHeight;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.min(Math.max(x, minX), maxX),
|
||||||
|
y: Math.min(Math.max(y, minY), maxY),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTranslate(el: HTMLElement, x: number, y: number) {
|
||||||
|
if (x === 0 && y === 0) {
|
||||||
|
el.style.translate = "";
|
||||||
|
} else {
|
||||||
|
el.style.translate = `${x}px ${y}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDraggableDialog() {
|
||||||
|
const offset = useRef<Position>({ x: 0, y: 0 });
|
||||||
|
const dragStart = useRef<Position | null>(null);
|
||||||
|
const initialOffset = useRef<Position>({ x: 0, y: 0 });
|
||||||
|
const contentRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const onPointerDown = useCallback((e: React.PointerEvent<HTMLElement>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
|
dragStart.current = { x: e.clientX, y: e.clientY };
|
||||||
|
initialOffset.current = { x: offset.current.x, y: offset.current.y };
|
||||||
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPointerMove = useCallback((e: React.PointerEvent<HTMLElement>) => {
|
||||||
|
if (!dragStart.current || !contentRef.current) return;
|
||||||
|
|
||||||
|
const dx = e.clientX - dragStart.current.x;
|
||||||
|
const dy = e.clientY - dragStart.current.y;
|
||||||
|
|
||||||
|
const rawX = initialOffset.current.x + dx;
|
||||||
|
const rawY = initialOffset.current.y + dy;
|
||||||
|
|
||||||
|
const el = contentRef.current;
|
||||||
|
const clamped = clampPosition(rawX, rawY, el.offsetWidth, el.offsetHeight);
|
||||||
|
|
||||||
|
offset.current = clamped;
|
||||||
|
applyTranslate(el, clamped.x, clamped.y);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPointerUp = useCallback((e: React.PointerEvent<HTMLElement>) => {
|
||||||
|
dragStart.current = null;
|
||||||
|
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetPosition = useCallback(() => {
|
||||||
|
offset.current = { x: 0, y: 0 };
|
||||||
|
if (contentRef.current) {
|
||||||
|
applyTranslate(contentRef.current, 0, 0);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dragHandleProps = {
|
||||||
|
onPointerDown,
|
||||||
|
onPointerMove,
|
||||||
|
onPointerUp,
|
||||||
|
style: { touchAction: "none" as const, cursor: "grab" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentRefCallback = useCallback((node: HTMLElement | null) => {
|
||||||
|
contentRef.current = node;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dragHandleProps,
|
||||||
|
contentRefCallback,
|
||||||
|
resetPosition,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user