- 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.
244 lines
7.2 KiB
Svelte
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>
|