Files
sgse-app/apps/web/src/lib/components/ErrorModal.svelte
deyvisonwanderley a3eab60fcd feat: enhance modal positioning and styling across components
- Implemented dynamic positioning for modals in ErrorModal, ComprovantePonto, and RegistroPonto components to ensure they are centered based on the viewport and container dimensions.
- Updated modal styles to improve visual consistency, including backdrop opacity adjustments and enhanced animations.
- Refactored modal handling logic to include scroll and resize event listeners for better responsiveness during user interactions.
2025-11-23 16:24:58 -03:00

244 lines
7.2 KiB
Svelte

<script lang="ts">
import { AlertCircle, X, HelpCircle } from 'lucide-svelte';
interface Props {
open: boolean;
title?: string;
message: string;
details?: string;
onClose: () => void;
}
let { open = $bindable(false), title = 'Erro', message, details, onClose }: Props = $props();
let modalPosition = $state<{ top: number; left: number } | null>(null);
// Função para calcular a posição baseada no container central da página
function calcularPosicaoModal() {
// Procurar pelo elemento container-central
const containerCentral = document.getElementById('container-central');
if (containerCentral) {
const rect = containerCentral.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Centralizar baseado na largura do container central
const containerCenterX = rect.left + (rect.width / 2);
const containerTop = rect.top;
// Posicionar o modal centralizado verticalmente na viewport, mas respeitando o container
const top = Math.max(50, Math.min(containerTop + 100, viewportHeight / 2));
return {
top: top,
left: containerCenterX
};
}
// Se não encontrar, usar posição padrão (centro da tela)
return null;
}
$effect(() => {
if (open) {
// Aguardar um pouco para garantir que o DOM está atualizado
setTimeout(() => {
modalPosition = calcularPosicaoModal();
}, 10);
// Adicionar listener de scroll para atualizar posição
const handleScroll = () => {
modalPosition = calcularPosicaoModal();
};
window.addEventListener('scroll', handleScroll, true);
window.addEventListener('resize', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll, true);
window.removeEventListener('resize', handleScroll);
};
}
});
// Função para obter estilo do modal baseado na posição calculada
function getModalStyle() {
if (modalPosition) {
// Garantir que o modal não saia da viewport
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const modalWidth = 700; // Aproximadamente max-w-2xl
const modalHeight = Math.min(viewportHeight * 0.9, 600);
let left = modalPosition.left;
let top = modalPosition.top;
// Ajustar se o modal sair da viewport à direita
if (left + (modalWidth / 2) > viewportWidth) {
left = viewportWidth - (modalWidth / 2) - 20;
}
// Ajustar se o modal sair da viewport à esquerda
if (left - (modalWidth / 2) < 20) {
left = (modalWidth / 2) + 20;
}
// Ajustar se o modal sair da viewport abaixo
if (top + modalHeight > viewportHeight) {
top = viewportHeight - modalHeight - 20;
}
// Ajustar se o modal sair da viewport acima
if (top < 20) {
top = 20;
}
return `position: fixed; top: ${top}px; left: ${left}px; transform: translate(-50%, 0); max-width: ${Math.min(modalWidth, viewportWidth - 40)}px;`;
}
// Se não houver posição calculada, centralizar na tela
return 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);';
}
// Verificar se details contém instruções ou apenas detalhes técnicos
const temInstrucoes = $derived.by(() => {
if (!details) return false;
// Se contém palavras-chave de instruções, é uma instrução
return details.includes('Por favor') ||
details.includes('aguarde') ||
details.includes('recarregue') ||
details.includes('Verifique') ||
details.includes('tente novamente') ||
details.match(/^\d+\./); // Começa com número (lista numerada)
});
function handleClose() {
open = false;
onClose();
}
</script>
{#if open}
<div
class="fixed inset-0 z-50 pointer-events-none"
style="animation: fadeIn 0.2s ease-out;"
role="dialog"
aria-modal="true"
aria-labelledby="modal-error-title"
>
<!-- Backdrop leve -->
<div
class="absolute inset-0 bg-black/20 transition-opacity duration-200 pointer-events-auto"
onclick={handleClose}
></div>
<!-- Modal Box -->
<div
class="absolute bg-base-100 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col z-10 transform transition-all duration-300 pointer-events-auto"
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); {getModalStyle()}"
onclick={(e) => e.stopPropagation()}
>
<!-- Header fixo -->
<div class="flex items-center justify-between px-6 py-4 border-b border-base-300 flex-shrink-0">
<h2 id="modal-error-title" class="text-error flex items-center gap-2 text-xl font-bold">
<AlertCircle class="h-6 w-6" strokeWidth={2.5} />
{title}
</h2>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300"
onclick={handleClose}
aria-label="Fechar"
>
<X class="h-5 w-5" />
</button>
</div>
<!-- Content com rolagem -->
<div class="flex-1 overflow-y-auto px-6 py-6 modal-scroll">
<!-- Mensagem principal -->
<div class="mb-6">
<p class="text-base-content text-base leading-relaxed font-medium">{message}</p>
</div>
<!-- Instruções ou detalhes (se houver) -->
{#if details}
<div class="bg-info/10 border-info/30 mb-4 rounded-lg border-l-4 p-4">
<div class="flex items-start gap-3">
<HelpCircle class="text-info h-5 w-5 shrink-0 mt-0.5" strokeWidth={2} />
<div class="flex-1">
<p class="text-base-content/90 text-sm font-semibold mb-2">
{temInstrucoes ? 'Como resolver:' : 'Informação adicional:'}
</p>
<div class="text-base-content/80 text-sm space-y-2">
{#each details.split('\n').filter(line => line.trim().length > 0) as linha (linha)}
{#if linha.trim().match(/^\d+\./)}
<div class="flex items-start gap-2">
<span class="text-info font-semibold shrink-0">{linha.trim().split('.')[0]}.</span>
<span class="flex-1 leading-relaxed">{linha.trim().substring(linha.trim().indexOf('.') + 1).trim()}</span>
</div>
{:else}
<p class="leading-relaxed">{linha.trim()}</p>
{/if}
{/each}
</div>
</div>
</div>
</div>
{/if}
</div>
<!-- Footer fixo -->
<div class="flex justify-end px-6 py-4 border-t border-base-300 flex-shrink-0">
<button class="btn btn-primary" onclick={handleClose}>Entendi, obrigado</button>
</div>
</div>
</div>
{/if}
<style>
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Scrollbar customizada para os modais */
:global(.modal-scroll) {
scrollbar-width: thin;
scrollbar-color: hsl(var(--bc) / 0.3) transparent;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
:global(.modal-scroll::-webkit-scrollbar) {
width: 8px;
}
:global(.modal-scroll::-webkit-scrollbar-track) {
background: transparent;
border-radius: 4px;
}
:global(.modal-scroll::-webkit-scrollbar-thumb) {
background-color: hsl(var(--bc) / 0.3);
border-radius: 4px;
transition: background-color 0.2s ease;
}
:global(.modal-scroll::-webkit-scrollbar-thumb:hover) {
background-color: hsl(var(--bc) / 0.5);
}
</style>