diff --git a/package.json b/package.json index f4003ca..ce68f18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "elo", - "version": "1.51.0", + "version": "1.51.1", "private": true, "type": "module", "scripts": { diff --git a/src/app/(authenticated)/admin/suggestions/page.tsx b/src/app/(authenticated)/admin/suggestions/page.tsx index 207e52c..ee80b05 100644 --- a/src/app/(authenticated)/admin/suggestions/page.tsx +++ b/src/app/(authenticated)/admin/suggestions/page.tsx @@ -1938,6 +1938,7 @@ export default function AdminSuggestionsPage() { // Menu de ações (clique direito no card): editar ou excluir const [actionsSuggestion, setActionsSuggestion] = useState(null) const [isActionsDialogOpen, setIsActionsDialogOpen] = useState(false) + const [actionsPosition, setActionsPosition] = useState<{ x: number; y: number } | null>(null) const deleteSuggestionMutation = api.suggestion.deleteAdmin.useMutation({ onSuccess: () => { @@ -1955,8 +1956,9 @@ export default function AdminSuggestionsPage() { }, }) - const openActionsDialog = (suggestion: SuggestionLocal) => { + const openActionsDialog = (suggestion: SuggestionLocal, position: { x: number; y: number }) => { setActionsSuggestion(suggestion) + setActionsPosition(position) setIsActionsDialogOpen(true) } @@ -2618,8 +2620,12 @@ export default function AdminSuggestionsPage() { open={isActionsDialogOpen} onOpenChange={(open) => { setIsActionsDialogOpen(open) - if (!open) setActionsSuggestion(null) + if (!open) { + setActionsSuggestion(null) + setActionsPosition(null) + } }} + position={actionsPosition} ideaNumber={actionsSuggestion?.ideaNumber} isDeleting={deleteSuggestionMutation.isPending} onEdit={() => { diff --git a/src/components/admin/suggestion/idea-actions-dialog.tsx b/src/components/admin/suggestion/idea-actions-dialog.tsx index 257a60a..949a17a 100644 --- a/src/components/admin/suggestion/idea-actions-dialog.tsx +++ b/src/components/admin/suggestion/idea-actions-dialog.tsx @@ -1,7 +1,7 @@ "use client" -import { useEffect, useState } from "react" -import { Dialog, DialogContent } from "@/components/ui/dialog" +import { useEffect, useLayoutEffect, useRef, useState } from "react" +import { createPortal } from "react-dom" import { Button } from "@/components/ui/button" import { Edit, Trash2, Loader2, AlertTriangle } from "lucide-react" @@ -12,6 +12,8 @@ function formatIdeaNumber(ideaNumber: number): string { export interface IdeaActionsDialogProps { open: boolean onOpenChange: (open: boolean) => void + /** Posição do cursor onde o menu deve surgir. */ + position?: { x: number; y: number } | null /** Número da ideia exibido no cabeçalho (ex.: #042). */ ideaNumber?: number /** Acionado ao escolher "Editar". */ @@ -22,38 +24,90 @@ export interface IdeaActionsDialogProps { isDeleting?: boolean } +const MENU_WIDTH = 224 // w-56 +const VIEWPORT_MARGIN = 8 + /** * Menu de ações de uma ideia (acionado por clique direito no card do kanban). - * Dialog enxuto, com sombra suave ao redor, oferecendo Editar ou Excluir - * (a exclusão exige confirmação dentro do próprio dialog). + * Surge discretamente ao lado do cursor — sem overlay escurecendo a tela — + * oferecendo Editar ou Excluir (a exclusão exige confirmação no próprio menu). */ export function IdeaActionsDialog({ open, onOpenChange, + position, ideaNumber, onEdit, onDelete, isDeleting = false, }: IdeaActionsDialogProps) { const [confirmingDelete, setConfirmingDelete] = useState(false) + const [mounted, setMounted] = useState(false) + const panelRef = useRef(null) + const [coords, setCoords] = useState<{ top: number; left: number }>({ top: 0, left: 0 }) + + useEffect(() => setMounted(true), []) - // Reinicia para a tela inicial sempre que o dialog reabrir. + // Reinicia para a tela inicial sempre que o menu fechar. useEffect(() => { if (!open) setConfirmingDelete(false) }, [open]) + // Fechar com ESC. + useEffect(() => { + if (!open) return + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onOpenChange(false) + } + window.addEventListener("keydown", onKey) + return () => window.removeEventListener("keydown", onKey) + }, [open, onOpenChange]) + + // Posiciona próximo ao cursor, mantendo o menu dentro da viewport. + useLayoutEffect(() => { + if (!open || !position) return + const panel = panelRef.current + const width = panel?.offsetWidth ?? MENU_WIDTH + const height = panel?.offsetHeight ?? 0 + let left = position.x + let top = position.y + if (left + width + VIEWPORT_MARGIN > window.innerWidth) { + left = window.innerWidth - width - VIEWPORT_MARGIN + } + if (top + height + VIEWPORT_MARGIN > window.innerHeight) { + top = window.innerHeight - height - VIEWPORT_MARGIN + } + setCoords({ + top: Math.max(VIEWPORT_MARGIN, top), + left: Math.max(VIEWPORT_MARGIN, left), + }) + }, [open, position, confirmingDelete]) + + if (!mounted || !open || !position) return null + const title = ideaNumber !== undefined ? `Ideia #${formatIdeaNumber(ideaNumber)}` : "Ideia" - return ( - - onOpenChange(false)} + onContextMenu={(e) => { + e.preventDefault() + onOpenChange(false) + }} + > +
e.stopPropagation()} + className="fixed w-56 overflow-hidden rounded-xl border border-border/60 bg-popover text-popover-foreground shadow-[0_12px_28px_-10px_rgba(0,0,0,0.4)] duration-150 animate-in fade-in-0 zoom-in-95" > {confirmingDelete ? ( -
+
-
+
@@ -63,7 +117,7 @@ export function IdeaActionsDialog({

-
+
) : ( -
-
+
+

{title}

)} - -
+ + , + document.body, ) } diff --git a/src/components/admin/suggestion/suggestions-kanban-board.tsx b/src/components/admin/suggestion/suggestions-kanban-board.tsx index 0d5c731..8ec7fc7 100644 --- a/src/components/admin/suggestion/suggestions-kanban-board.tsx +++ b/src/components/admin/suggestion/suggestions-kanban-board.tsx @@ -42,7 +42,7 @@ export interface SuggestionsKanbanBoardProps< onDragEnd: OnDragEndResponder onOpenSuggestion: (suggestion: T) => void /** Clique direito sobre um card — recebe a ideia e a posição do cursor. */ - onContextMenuSuggestion?: (suggestion: T) => void + onContextMenuSuggestion?: (suggestion: T, position: { x: number; y: number }) => void } const SKELETON_CARDS_PER_COLUMN = 3 @@ -155,7 +155,7 @@ export function SuggestionsKanbanBoard({ onContextMenu={(e) => { if (!onContextMenuSuggestion) return e.preventDefault() - onContextMenuSuggestion(s) + onContextMenuSuggestion(s, { x: e.clientX, y: e.clientY }) }} >