feat: implement dynamic theme support across chat components, enhancing UI consistency with reactive color updates and gradient functionalities

This commit is contained in:
2025-12-22 15:13:07 -03:00
parent f6bf4ec918
commit e03b6d7a65
5 changed files with 367 additions and 42 deletions

View File

@@ -17,6 +17,7 @@
import ChatList from './ChatList.svelte';
import ChatWindow from './ChatWindow.svelte';
import { MessageSquare, Minus, Maximize2, X, Bell } from 'lucide-svelte';
import { obterCoresDoTema, obterTemaPersistidoNoLocalStorage } from '$lib/utils/temas';
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
@@ -955,6 +956,80 @@
window.removeEventListener('touchend', handleTouchEnd);
};
});
// Obter cores do tema atual (reativo)
let coresTema = $state(obterCoresDoTema());
// Atualizar cores quando o tema mudar
$effect(() => {
if (typeof window === 'undefined') return;
const atualizarCores = () => {
coresTema = obterCoresDoTema();
};
// Atualizar cores inicialmente
atualizarCores();
// Escutar mudanças de tema
window.addEventListener('themechange', atualizarCores);
// Observar mudanças no atributo data-theme do HTML
const observer = new MutationObserver(atualizarCores);
const htmlElement = document.documentElement;
observer.observe(htmlElement, {
attributes: true,
attributeFilter: ['data-theme']
});
return () => {
window.removeEventListener('themechange', atualizarCores);
observer.disconnect();
};
});
// Função para obter gradiente do tema
function obterGradienteTema() {
const primary = coresTema.primary;
// Criar variações da cor primária para o gradiente
return `linear-gradient(135deg, ${primary} 0%, ${primary}dd 50%, ${primary}bb 100%)`;
}
// Função para obter rgba da cor primária
function obterPrimariaRgba(alpha: number = 1) {
const primary = coresTema.primary.trim();
// Se já for rgba, extrair os valores
if (primary.startsWith('rgba')) {
const match = primary.match(/rgba?\(([^)]+)\)/);
if (match) {
const values = match[1].split(',').map(v => v.trim());
if (values.length >= 3) {
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${alpha})`;
}
}
}
// Se for hex, converter
if (primary.startsWith('#')) {
const hex = primary.replace('#', '');
if (hex.length === 6) {
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
}
// Se for hsl, converter para hsla
if (primary.startsWith('hsl')) {
const match = primary.match(/hsl\(([^)]+)\)/);
if (match) {
return `hsla(${match[1]}, ${alpha})`;
}
// Fallback: tentar adicionar alpha
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
}
// Fallback padrão
return `rgba(102, 126, 234, ${alpha})`;
}
</script>
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
@@ -975,10 +1050,10 @@
bottom: {bottomPos};
right: {rightPos};
position: fixed !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background: {obterGradienteTema()};
box-shadow:
0 20px 60px -10px rgba(102, 126, 234, 0.5),
0 10px 30px -5px rgba(118, 75, 162, 0.4),
0 20px 60px -10px {obterPrimariaRgba(0.5)},
0 10px 30px -5px {obterPrimariaRgba(0.4)},
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
border-radius: 50%;
cursor: {isDragging ? 'grabbing' : 'grab'};
@@ -1058,17 +1133,17 @@
strokeWidth={2}
/>
<!-- Badge ULTRA PREMIUM com gradiente e brilho -->
<!-- Badge ULTRA PREMIUM com gradiente e brilho usando cores do tema -->
{#if count?.data && count.data > 0}
<span
class="absolute -top-1 -right-1 z-20 flex h-8 w-8 items-center justify-center rounded-full text-xs font-black text-white"
style="
background: linear-gradient(135deg, #ff416c, #ff4b2b);
background: {coresTema.error ? `linear-gradient(135deg, ${coresTema.error}, ${coresTema.error}dd)` : 'linear-gradient(135deg, #ff416c, #ff4b2b)'};
box-shadow:
0 8px 24px -4px rgba(255, 65, 108, 0.6),
0 4px 12px -2px rgba(255, 75, 43, 0.4),
0 8px 24px -4px {coresTema.error ? obterPrimariaRgba(0.6).replace(coresTema.primary, coresTema.error) : 'rgba(255, 65, 108, 0.6)'},
0 4px 12px -2px {coresTema.error ? obterPrimariaRgba(0.4).replace(coresTema.primary, coresTema.error) : 'rgba(255, 75, 43, 0.4)'},
0 0 0 3px rgba(255, 255, 255, 0.3),
0 0 0 5px rgba(255, 65, 108, 0.2);
0 0 0 5px {coresTema.error ? obterPrimariaRgba(0.2).replace(coresTema.primary, coresTema.error) : 'rgba(255, 65, 108, 0.2)'};
animation: badge-bounce 2s ease-in-out infinite;
"
>
@@ -1121,8 +1196,8 @@
<div
class="relative flex items-center justify-between overflow-hidden px-6 py-5 text-white"
style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
box-shadow: 0 8px 32px -4px rgba(102, 126, 234, 0.3);
background: {obterGradienteTema()};
box-shadow: 0 8px 32px -4px {obterPrimariaRgba(0.3)};
cursor: {isDragging ? 'grabbing' : 'grab'};
"
onmousedown={handleMouseDown}
@@ -1239,7 +1314,8 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda superior"
class="hover:bg-primary/20 absolute top-0 right-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
class="absolute top-0 right-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}"
onmousedown={(e) => handleResizeStart(e, 'n')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')}
style="border-radius: 24px 24px 0 0;"
@@ -1249,7 +1325,8 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda inferior"
class="hover:bg-primary/20 absolute right-0 bottom-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
class="absolute right-0 bottom-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}"
onmousedown={(e) => handleResizeStart(e, 's')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')}
style="border-radius: 0 0 24px 24px;"
@@ -1259,7 +1336,8 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda esquerda"
class="hover:bg-primary/20 absolute top-0 bottom-0 left-0 z-50 w-2 cursor-ew-resize transition-colors"
class="absolute top-0 bottom-0 left-0 z-50 w-2 cursor-ew-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}"
onmousedown={(e) => handleResizeStart(e, 'w')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')}
style="border-radius: 24px 0 0 24px;"
@@ -1269,7 +1347,8 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda direita"
class="hover:bg-primary/20 absolute top-0 right-0 bottom-0 z-50 w-2 cursor-ew-resize transition-colors"
class="absolute top-0 right-0 bottom-0 z-50 w-2 cursor-ew-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}"
onmousedown={(e) => handleResizeStart(e, 'e')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')}
style="border-radius: 0 24px 24px 0;"
@@ -1279,7 +1358,8 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto superior esquerdo"
class="hover:bg-primary/20 absolute top-0 left-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
class="absolute top-0 left-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}"
onmousedown={(e) => handleResizeStart(e, 'nw')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')}
style="border-radius: 24px 0 0 0;"
@@ -1288,7 +1368,8 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto superior direito"
class="hover:bg-primary/20 absolute top-0 right-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
class="absolute top-0 right-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}"
onmousedown={(e) => handleResizeStart(e, 'ne')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')}
style="border-radius: 0 24px 0 0;"
@@ -1297,7 +1378,8 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto inferior esquerdo"
class="hover:bg-primary/20 absolute bottom-0 left-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
class="absolute bottom-0 left-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}"
onmousedown={(e) => handleResizeStart(e, 'sw')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')}
style="border-radius: 0 0 0 24px;"
@@ -1306,7 +1388,8 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto inferior direito"
class="hover:bg-primary/20 absolute right-0 bottom-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
class="absolute right-0 bottom-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
style="--hover-bg: {obterPrimariaRgba(0.2)}"
onmousedown={(e) => handleResizeStart(e, 'se')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')}
style="border-radius: 0 0 24px 0;"
@@ -1324,8 +1407,8 @@
role="button"
tabindex="0"
aria-label="Abrir conversa: Nova mensagem de {notificationMsg.remetente}"
class="bg-base-100 border-primary/20 fixed top-4 right-4 z-1000 max-w-sm cursor-pointer rounded-lg border p-4 shadow-2xl"
style="box-shadow: 0 10px 40px -10px rgba(0,0,0,0.3); animation: slideInRight 0.3s ease-out;"
class="bg-base-100 fixed top-4 right-4 z-1000 max-w-sm cursor-pointer rounded-lg border p-4 shadow-2xl"
style="border-color: {obterPrimariaRgba(0.2)}; box-shadow: 0 10px 40px -10px rgba(0,0,0,0.3); animation: slideInRight 0.3s ease-out;"
onclick={() => {
const conversaIdToOpen = notificationMsg?.conversaId;
showGlobalNotificationPopup = false;
@@ -1356,8 +1439,8 @@
}}
>
<div class="flex items-start gap-3">
<div class="bg-primary/20 flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
<Bell class="text-primary h-5 w-5" strokeWidth={2} />
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full" style="background-color: {obterPrimariaRgba(0.2)}">
<Bell class="h-5 w-5" style="color: {coresTema.primary}" strokeWidth={2} />
</div>
<div class="min-w-0 flex-1">
<p class="text-base-content mb-1 text-sm font-semibold">
@@ -1366,7 +1449,7 @@
<p class="text-base-content/70 line-clamp-2 text-xs">
{notificationMsg.conteudo}
</p>
<p class="text-primary mt-1 text-xs">Clique para abrir</p>
<p class="mt-1 text-xs" style="color: {coresTema.primary}">Clique para abrir</p>
</div>
<button
type="button"
@@ -1414,18 +1497,23 @@
}
}
/* Ondas de pulso para o botão flutuante */
/* Ondas de pulso para o botão flutuante - cores dinâmicas */
@keyframes pulse-ring {
0% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.5);
box-shadow: 0 0 0 0 var(--pulse-color, rgba(102, 126, 234, 0.5));
}
50% {
box-shadow: 0 0 0 15px rgba(102, 126, 234, 0);
box-shadow: 0 0 0 15px transparent;
}
100% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
box-shadow: 0 0 0 0 var(--pulse-color, rgba(102, 126, 234, 0.5));
}
}
/* Estilos para handles de redimensionamento com hover dinâmico */
[style*="--hover-bg"]:hover {
background-color: var(--hover-bg) !important;
}
/* Rotação para anel de brilho - suavizada */
@keyframes rotate {