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

@@ -9,6 +9,7 @@
import NewConversationModal from './NewConversationModal.svelte'; import NewConversationModal from './NewConversationModal.svelte';
import { Search, Plus, MessageSquare, Users, UsersRound } from 'lucide-svelte'; import { Search, Plus, MessageSquare, Users, UsersRound } from 'lucide-svelte';
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
import { obterCoresDoTema } from '$lib/utils/temas';
const client = useConvexClient(); const client = useConvexClient();
@@ -23,6 +24,57 @@
let searchQuery = $state(''); let searchQuery = $state('');
let activeTab = $state<'usuarios' | 'conversas'>('usuarios'); let activeTab = $state<'usuarios' | 'conversas'>('usuarios');
// 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();
};
atualizarCores();
window.addEventListener('themechange', atualizarCores);
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 rgba da cor primária
function obterPrimariaRgba(alpha: number = 1) {
const primary = coresTema.primary;
if (primary.startsWith('rgba')) {
const match = primary.match(/rgba?\(([^)]+)\)/);
if (match) {
const values = match[1].split(',');
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${alpha})`;
}
}
if (primary.startsWith('#')) {
const hex = primary.replace('#', '');
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})`;
}
if (primary.startsWith('hsl')) {
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
}
return `rgba(102, 126, 234, ${alpha})`;
}
// Debug: monitorar carregamento de dados // Debug: monitorar carregamento de dados
$effect(() => { $effect(() => {
@@ -263,7 +315,7 @@
<!-- Ícone de mensagem --> <!-- Ícone de mensagem -->
<div <div
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-all duration-300 hover:scale-110" class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-all duration-300 hover:scale-110"
style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border: 1px solid rgba(102, 126, 234, 0.2);" style="background: linear-gradient(135deg, {obterPrimariaRgba(0.1)} 0%, {obterPrimariaRgba(0.1)} 100%); border: 1px solid {obterPrimariaRgba(0.2)};"
> >
<MessageSquare class="text-primary h-5 w-5" strokeWidth={2} /> <MessageSquare class="text-primary h-5 w-5" strokeWidth={2} />
</div> </div>

View File

@@ -17,6 +17,7 @@
import ChatList from './ChatList.svelte'; import ChatList from './ChatList.svelte';
import ChatWindow from './ChatWindow.svelte'; import ChatWindow from './ChatWindow.svelte';
import { MessageSquare, Minus, Maximize2, X, Bell } from 'lucide-svelte'; import { MessageSquare, Minus, Maximize2, X, Bell } from 'lucide-svelte';
import { obterCoresDoTema, obterTemaPersistidoNoLocalStorage } from '$lib/utils/temas';
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {}); const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
@@ -955,6 +956,80 @@
window.removeEventListener('touchend', handleTouchEnd); 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> </script>
<!-- Botão flutuante MODERNO E ARRASTÁVEL --> <!-- Botão flutuante MODERNO E ARRASTÁVEL -->
@@ -975,10 +1050,10 @@
bottom: {bottomPos}; bottom: {bottomPos};
right: {rightPos}; right: {rightPos};
position: fixed !important; position: fixed !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); background: {obterGradienteTema()};
box-shadow: box-shadow:
0 20px 60px -10px rgba(102, 126, 234, 0.5), 0 20px 60px -10px {obterPrimariaRgba(0.5)},
0 10px 30px -5px rgba(118, 75, 162, 0.4), 0 10px 30px -5px {obterPrimariaRgba(0.4)},
0 0 0 1px rgba(255, 255, 255, 0.1) inset; 0 0 0 1px rgba(255, 255, 255, 0.1) inset;
border-radius: 50%; border-radius: 50%;
cursor: {isDragging ? 'grabbing' : 'grab'}; cursor: {isDragging ? 'grabbing' : 'grab'};
@@ -1058,17 +1133,17 @@
strokeWidth={2} 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} {#if count?.data && count.data > 0}
<span <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" 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=" 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: box-shadow:
0 8px 24px -4px rgba(255, 65, 108, 0.6), 0 8px 24px -4px {coresTema.error ? obterPrimariaRgba(0.6).replace(coresTema.primary, coresTema.error) : 'rgba(255, 65, 108, 0.6)'},
0 4px 12px -2px rgba(255, 75, 43, 0.4), 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 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; animation: badge-bounce 2s ease-in-out infinite;
" "
> >
@@ -1121,8 +1196,8 @@
<div <div
class="relative flex items-center justify-between overflow-hidden px-6 py-5 text-white" class="relative flex items-center justify-between overflow-hidden px-6 py-5 text-white"
style=" style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); background: {obterGradienteTema()};
box-shadow: 0 8px 32px -4px rgba(102, 126, 234, 0.3); box-shadow: 0 8px 32px -4px {obterPrimariaRgba(0.3)};
cursor: {isDragging ? 'grabbing' : 'grab'}; cursor: {isDragging ? 'grabbing' : 'grab'};
" "
onmousedown={handleMouseDown} onmousedown={handleMouseDown}
@@ -1239,7 +1314,8 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda superior" 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')} onmousedown={(e) => handleResizeStart(e, 'n')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')}
style="border-radius: 24px 24px 0 0;" style="border-radius: 24px 24px 0 0;"
@@ -1249,7 +1325,8 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda inferior" 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')} onmousedown={(e) => handleResizeStart(e, 's')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')}
style="border-radius: 0 0 24px 24px;" style="border-radius: 0 0 24px 24px;"
@@ -1259,7 +1336,8 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda esquerda" 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')} onmousedown={(e) => handleResizeStart(e, 'w')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')}
style="border-radius: 24px 0 0 24px;" style="border-radius: 24px 0 0 24px;"
@@ -1269,7 +1347,8 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda direita" 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')} onmousedown={(e) => handleResizeStart(e, 'e')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')}
style="border-radius: 0 24px 24px 0;" style="border-radius: 0 24px 24px 0;"
@@ -1279,7 +1358,8 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto superior esquerdo" 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')} onmousedown={(e) => handleResizeStart(e, 'nw')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')}
style="border-radius: 24px 0 0 0;" style="border-radius: 24px 0 0 0;"
@@ -1288,7 +1368,8 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto superior direito" 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')} onmousedown={(e) => handleResizeStart(e, 'ne')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')}
style="border-radius: 0 24px 0 0;" style="border-radius: 0 24px 0 0;"
@@ -1297,7 +1378,8 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto inferior esquerdo" 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')} onmousedown={(e) => handleResizeStart(e, 'sw')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')}
style="border-radius: 0 0 0 24px;" style="border-radius: 0 0 0 24px;"
@@ -1306,7 +1388,8 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto inferior direito" 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')} onmousedown={(e) => handleResizeStart(e, 'se')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')}
style="border-radius: 0 0 24px 0;" style="border-radius: 0 0 24px 0;"
@@ -1324,8 +1407,8 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Abrir conversa: Nova mensagem de {notificationMsg.remetente}" 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" class="bg-base-100 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;" 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={() => { onclick={() => {
const conversaIdToOpen = notificationMsg?.conversaId; const conversaIdToOpen = notificationMsg?.conversaId;
showGlobalNotificationPopup = false; showGlobalNotificationPopup = false;
@@ -1356,8 +1439,8 @@
}} }}
> >
<div class="flex items-start gap-3"> <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"> <div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full" style="background-color: {obterPrimariaRgba(0.2)}">
<Bell class="text-primary h-5 w-5" strokeWidth={2} /> <Bell class="h-5 w-5" style="color: {coresTema.primary}" strokeWidth={2} />
</div> </div>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-base-content mb-1 text-sm font-semibold"> <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"> <p class="text-base-content/70 line-clamp-2 text-xs">
{notificationMsg.conteudo} {notificationMsg.conteudo}
</p> </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> </div>
<button <button
type="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 { @keyframes pulse-ring {
0% { 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% { 50% {
box-shadow: 0 0 0 15px rgba(102, 126, 234, 0); box-shadow: 0 0 0 15px transparent;
} }
100% { 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 */ /* Rotação para anel de brilho - suavizada */
@keyframes rotate { @keyframes rotate {

View File

@@ -30,6 +30,7 @@
//import { getAvatarUrl } from '$lib/utils/avatarGenerator'; //import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { voltarParaLista } from '$lib/stores/chatStore'; import { voltarParaLista } from '$lib/stores/chatStore';
import { useConvexClient, useQuery } from 'convex-svelte'; import { useConvexClient, useQuery } from 'convex-svelte';
import { obterCoresDoTema } from '$lib/utils/temas';
//import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte'; //import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
@@ -260,23 +261,84 @@
let souAnfitriao = $derived( let souAnfitriao = $derived(
chamadaAtual && meuPerfil?.data ? chamadaAtual.criadoPor === meuPerfil.data._id : false chamadaAtual && meuPerfil?.data ? chamadaAtual.criadoPor === meuPerfil.data._id : false
); );
// 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();
};
atualizarCores();
window.addEventListener('themechange', atualizarCores);
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 rgba da cor primária
function obterPrimariaRgba(alpha: number = 1) {
const primary = coresTema.primary.trim();
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})`;
}
}
}
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})`;
}
}
if (primary.startsWith('hsl')) {
const match = primary.match(/hsl\(([^)]+)\)/);
if (match) {
return `hsla(${match[1]}, ${alpha})`;
}
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
}
return `rgba(102, 126, 234, ${alpha})`;
}
</script> </script>
<div class="flex h-full flex-col" onclick={() => (showAdminMenu = false)}> <div class="flex h-full flex-col" onclick={() => (showAdminMenu = false)}>
<!-- Header --> <!-- Header -->
<div <div
class="border-base-300 bg-base-200 flex items-center gap-3 border-b px-4 py-3" class="border-base-300 flex items-center gap-3 border-b px-4 py-3"
style="background-color: {coresTema.base200}; border-color: {coresTema.base300};"
onclick={(e) => e.stopPropagation()} onclick={(e) => e.stopPropagation()}
> >
<!-- Botão Voltar --> <!-- Botão Voltar -->
<button <button
type="button" type="button"
class="btn btn-sm btn-circle hover:bg-primary/20 transition-all duration-200 hover:scale-110" class="btn btn-sm btn-circle transition-all duration-200 hover:scale-110"
style="--hover-bg: {obterPrimariaRgba(0.2)}"
onclick={voltarParaLista} onclick={voltarParaLista}
aria-label="Voltar" aria-label="Voltar"
title="Voltar para lista de conversas" title="Voltar para lista de conversas"
> >
<ArrowLeft class="text-primary h-6 w-6" strokeWidth={2.5} /> <ArrowLeft class="h-6 w-6" style="color: {coresTema.primary}" strokeWidth={2.5} />
</button> </button>
<!-- Avatar e Info --> <!-- Avatar e Info -->
@@ -289,7 +351,7 @@
userId={conversa()?.outroUsuario?._id} userId={conversa()?.outroUsuario?._id}
/> />
{:else} {:else}
<div class="bg-primary/20 flex h-10 w-10 items-center justify-center rounded-full text-xl"> <div class="flex h-10 w-10 items-center justify-center rounded-full text-xl" style="background-color: {obterPrimariaRgba(0.2)}">
{getAvatarConversa()} {getAvatarConversa()}
</div> </div>
{/if} {/if}
@@ -380,7 +442,8 @@
</div> </div>
{#if conversa()?.tipo === 'sala_reuniao' && isAdmin?.data} {#if conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
<span <span
class="text-primary ml-1 text-[10px] font-semibold whitespace-nowrap" class="ml-1 text-[10px] font-semibold whitespace-nowrap"
style="color: {coresTema.primary}"
title="Você é administrador desta sala">• Admin</span title="Você é administrador desta sala">• Admin</span
> >
{/if} {/if}
@@ -727,10 +790,8 @@
{#each searchResults as resultado, index (resultado._id)} {#each searchResults as resultado, index (resultado._id)}
<button <button
type="button" type="button"
class="hover:bg-base-300 flex w-full items-start gap-3 px-4 py-3 text-left transition-colors {index === class="hover:bg-base-300 flex w-full items-start gap-3 px-4 py-3 text-left transition-colors"
selectedSearchResult style={index === selectedSearchResult ? `background-color: ${obterPrimariaRgba(0.1)}` : ''}
? 'bg-primary/10'
: ''}"
onclick={() => { onclick={() => {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('scrollToMessage', { new CustomEvent('scrollToMessage', {
@@ -745,7 +806,8 @@
aria-label="Mensagem de {resultado.remetente?.nome || 'Usuário'}" aria-label="Mensagem de {resultado.remetente?.nome || 'Usuário'}"
> >
<div <div
class="bg-primary/20 flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full" class="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full"
style="background-color: {obterPrimariaRgba(0.2)}"
> >
{#if resultado.remetente?.fotoPerfilUrl} {#if resultado.remetente?.fotoPerfilUrl}
<img <img
@@ -840,7 +902,7 @@
<div class="modal-box max-w-md" onclick={(e) => e.stopPropagation()}> <div class="modal-box max-w-md" onclick={(e) => e.stopPropagation()}>
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4"> <div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
<h2 class="flex items-center gap-2 text-xl font-semibold"> <h2 class="flex items-center gap-2 text-xl font-semibold">
<Bell class="text-primary h-5 w-5" /> <Bell class="h-5 w-5" style="color: {coresTema.primary}" />
Enviar Notificação Enviar Notificação
</h2> </h2>
<button <button
@@ -931,3 +993,10 @@
details={errorInstructions || errorDetails} details={errorInstructions || errorDetails}
onClose={fecharErrorModal} onClose={fecharErrorModal}
/> />
<style>
/* Estilos para hover dinâmico com cores do tema */
[style*="--hover-bg"]:hover {
background-color: var(--hover-bg) !important;
}
</style>

View File

@@ -13,6 +13,7 @@
type EncryptedMessage type EncryptedMessage
} from '$lib/utils/e2eEncryption'; } from '$lib/utils/e2eEncryption';
import { obterChaveCriptografia, armazenarChaveCriptografia } from '$lib/stores/chatStore'; import { obterChaveCriptografia, armazenarChaveCriptografia } from '$lib/stores/chatStore';
import { obterCoresDoTema } from '$lib/utils/temas';
interface Props { interface Props {
conversaId: Id<'conversas'>; conversaId: Id<'conversas'>;
@@ -88,6 +89,63 @@
let mentionStartPos = $state(0); let mentionStartPos = $state(0);
let selectedMentionIndex = $state(0); // Índice do participante selecionado no dropdown let selectedMentionIndex = $state(0); // Índice do participante selecionado no dropdown
let mensagemMuitoLonga = $state(false); let mensagemMuitoLonga = $state(false);
// 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();
};
atualizarCores();
window.addEventListener('themechange', atualizarCores);
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 rgba da cor primária
function obterPrimariaRgba(alpha: number = 1) {
const primary = coresTema.primary;
if (primary.startsWith('rgba')) {
const match = primary.match(/rgba?\(([^)]+)\)/);
if (match) {
const values = match[1].split(',');
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${alpha})`;
}
}
if (primary.startsWith('#')) {
const hex = primary.replace('#', '');
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})`;
}
if (primary.startsWith('hsl')) {
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
}
return `rgba(102, 126, 234, ${alpha})`;
}
// Função para obter gradiente do tema
function obterGradienteTema() {
const primary = coresTema.primary;
return `linear-gradient(135deg, ${primary} 0%, ${primary}dd 100%)`;
}
// Emojis mais usados // Emojis mais usados
const emojis = [ const emojis = [
@@ -670,7 +728,7 @@
<div class="relative shrink-0"> <div class="relative shrink-0">
<label <label
class="group relative flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-xl transition-all duration-300" class="group relative flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-xl transition-all duration-300"
style="background: rgba(102, 126, 234, 0.1); border: 1px solid rgba(102, 126, 234, 0.2);" style="background: {obterPrimariaRgba(0.1)}; border: 1px solid {obterPrimariaRgba(0.2)};"
title="Anexar arquivo" title="Anexar arquivo"
aria-label="Anexar arquivo" aria-label="Anexar arquivo"
> >
@@ -838,7 +896,7 @@
<button <button
type="button" type="button"
class="group relative flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-xl transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50" class="group relative flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-xl transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);" style="background: {obterGradienteTema()}; box-shadow: 0 8px 24px -4px {obterPrimariaRgba(0.4)};"
onclick={handleEnviar} onclick={handleEnviar}
disabled={!mensagem.trim() || enviando || uploadingFile} disabled={!mensagem.trim() || enviando || uploadingFile}
aria-label="Enviar mensagem" aria-label="Enviar mensagem"

View File

@@ -5,6 +5,7 @@
import { format } from 'date-fns'; import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale'; import { ptBR } from 'date-fns/locale';
import { Clock, Trash2, X } from 'lucide-svelte'; import { Clock, Trash2, X } from 'lucide-svelte';
import { obterCoresDoTema } from '$lib/utils/temas';
interface Props { interface Props {
conversaId: Id<'conversas'>; conversaId: Id<'conversas'>;
@@ -22,6 +23,63 @@
let data = $state(''); let data = $state('');
let hora = $state(''); let hora = $state('');
let loading = $state(false); let loading = $state(false);
// 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();
};
atualizarCores();
window.addEventListener('themechange', atualizarCores);
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 rgba da cor primária
function obterPrimariaRgba(alpha: number = 1) {
const primary = coresTema.primary;
if (primary.startsWith('rgba')) {
const match = primary.match(/rgba?\(([^)]+)\)/);
if (match) {
const values = match[1].split(',');
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${alpha})`;
}
}
if (primary.startsWith('#')) {
const hex = primary.replace('#', '');
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})`;
}
if (primary.startsWith('hsl')) {
return primary.replace(/\)$/, `, ${alpha})`).replace('hsl', 'hsla');
}
return `rgba(102, 126, 234, ${alpha})`;
}
// Função para obter gradiente do tema
function obterGradienteTema() {
const primary = coresTema.primary;
return `linear-gradient(135deg, ${primary} 0%, ${primary}dd 100%)`;
}
// Rastrear mudanças nas mensagens agendadas // Rastrear mudanças nas mensagens agendadas
$effect(() => { $effect(() => {
@@ -186,7 +244,7 @@
<button <button
type="button" type="button"
class="group relative overflow-hidden rounded-xl px-6 py-3 font-bold text-white transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50" class="group relative overflow-hidden rounded-xl px-6 py-3 font-bold text-white transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);" style="background: {obterGradienteTema()}; box-shadow: 0 8px 24px -4px {obterPrimariaRgba(0.4)};"
onclick={handleAgendar} onclick={handleAgendar}
disabled={loading || !mensagem.trim() || !data || !hora} disabled={loading || !mensagem.trim() || !data || !hora}
> >