MelissaSeguranca: pagina nativa 2-col + grupo "Conta" sai inteiro de Configuracoes

Tira "Seguranca" do MelissaConfiguracoes (era embed cfg-seguranca ->
SecurityPage.vue) e cria a /melissa/seguranca nativa Melissa.

Sidebar (mse-side):
- Card "Estado da conta" — email (mono) + tag "Sessão Ativa" pulsando
  + warning amber sobre desconectar todos dispositivos
- Card "Boas praticas" — 4 dicas com bullet colorido (8+ chars,
  evite obvio, encerre sessao publica, nao reuse senhas)

Main:
- Card "Trocar senha" — 3 Password inputs (atual + nova + confirmar)
  + barra de forca 4-segmentos (Muito fraca/Fraca/Boa/Forte) +
  match indicator (check verde / x amber) + warning + 2 botoes:
  "Enviar link por e-mail" (reset por email) + primary "Trocar senha"
- Estado "concluido" com check verde + redirect pro login

Logica espelhada do SecurityPage:
- changePassword: signInWithPassword pra reautenticar + updateUser +
  hardLogout (signOut global + clear sb-* tokens) apos 2.6s
- sendResetEmail: resetPasswordForEmail com redirectTo /auth/reset-password

Wire-up:
- MelissaLayout: import + render `<MelissaSeguranca v-if=secaoAberta=='seguranca'>`
- 'seguranca' sai de MELISSA_CONFIG_ALIASES, entra em MELISSA_NON_CONFIG_SLUGS
- SECOES.seguranca atualizado (label + descricao + duplicado removido)
- MelissaConfiguracoes: cfg-seguranca removido de COMPONENT_MAP +
  ROUTE_ALIASES; grupo "Conta" inteiro removido (Perfil/Plano/Negocio/
  Seguranca todos viraram nativos agora)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-06 15:57:27 -03:00
parent 7c0c1b3528
commit 2ca9cde2ea
3 changed files with 979 additions and 20 deletions
+3 -14
View File
@@ -39,7 +39,6 @@ const emit = defineEmits(['close']);
// existir, senão a key como-é.
const ROUTE_ALIASES = {
aparencia: 'aparencia',
seguranca: 'cfg-seguranca',
bloqueios: 'cfg-bloqueios'
};
// URLs antigas (/melissa/fundo, /melissa/relogio, /melissa/cronometro)
@@ -99,7 +98,7 @@ const COMPONENT_MAP = {
// 'cfg-perfil' removido — virou pagina nativa MelissaPerfil em /melissa/perfil
// 'cfg-plano' removido — virou pagina nativa MelissaPlano em /melissa/plano
// 'cfg-negocio' removido — virou pagina nativa MelissaNegocio em /melissa/negocio
'cfg-seguranca': defineAsyncComponent(() => import('@/views/pages/auth/SecurityPage.vue'))
// 'cfg-seguranca' removido — virou pagina nativa MelissaSeguranca em /melissa/seguranca
};
// Keys que renderizam controles inline definidos neste arquivo (Layout Melissa).
@@ -145,18 +144,8 @@ const grupos = [
{ key: 'aparencia', label: 'Layout Melissa', desc: 'Tema, cor primária, surface, plano de fundo, relógio e cronômetro — tudo numa tela só.', icon: 'pi pi-palette' }
]
},
{
key: 'conta',
label: 'Conta',
desc: 'Segurança da sua conta.',
icon: 'pi pi-user',
items: [
// "Meu Perfil" virou pagina nativa em /melissa/perfil (MelissaPerfil)
// "Meu Plano" virou pagina nativa em /melissa/plano (MelissaPlano)
// "Meu Negócio" virou pagina nativa em /melissa/negocio (MelissaNegocio)
{ key: 'cfg-seguranca', label: 'Segurança', desc: 'Senha, dispositivos confiáveis e sessões ativas.', icon: 'pi pi-shield' }
]
},
// Grupo "Conta" inteiro saiu — todos os 4 items (Perfil/Plano/
// Negocio/Seguranca) viraram paginas nativas no /melissa/<slug>.
{
key: 'agenda',
label: 'Agenda',
+10 -3
View File
@@ -37,6 +37,7 @@ import MelissaPerfil from './MelissaPerfil.vue';
import MelissaPlano from './MelissaPlano.vue';
import MelissaNegocio from './MelissaNegocio.vue';
import MelissaAlterarPlano from './MelissaAlterarPlano.vue';
import MelissaSeguranca from './MelissaSeguranca.vue';
import MelissaEmbed from './MelissaEmbed.vue';
import MelissaCadastrosRecebidos from './MelissaCadastrosRecebidos.vue';
import MelissaAgendamentosRecebidos from './MelissaAgendamentosRecebidos.vue';
@@ -173,9 +174,10 @@ const SECOES = {
plano: { label: 'Meu Plano', icon: 'pi pi-credit-card', descricao: 'Assinatura, recursos liberados e historico de mudancas.' },
// Pagina nativa do negocio (MelissaNegocio) — saiu do MelissaConfiguracoes
negocio: { label: 'Meu Negócio', icon: 'pi pi-briefcase', descricao: 'Identidade, fiscal, endereco, contato, redes — gamificacao no aside.' },
// Pagina nativa de seguranca (MelissaSeguranca) — saiu do MelissaConfiguracoes
seguranca: { label: 'Segurança', icon: 'pi pi-shield', descricao: 'Trocar senha + boas praticas + estado da sessao.' },
// Pagina nativa de alterar plano (MelissaAlterarPlano) — substitui /therapist/upgrade
'alterar-plano': { label: 'Alterar Plano', icon: 'pi pi-arrow-up-right', descricao: 'Escolha um plano pessoal pra ativar todos os recursos.' },
seguranca: { label: 'Segurança', icon: 'pi pi-shield', descricao: 'Senha, dispositivos confiáveis e sessões.' },
// Onda 1 — pages embedadas via MelissaEmbed (1-coluna, hero glass)
'financeiro': { label: 'Financeiro', icon: 'pi pi-wallet', descricao: 'Visão geral, recebíveis e indicadores.' },
'financeiro-lancamentos': { label: 'Lançamentos financeiros', icon: 'pi pi-list', descricao: 'Cobranças, pagamentos e recebimentos.' },
@@ -201,13 +203,13 @@ const MELISSA_NON_CONFIG_SLUGS = new Set([
'tags', 'grupos', 'cadastros-recebidos', 'medicos', 'agendamentos-recebidos',
'link-externo', 'notificacoes', 'financeiro', 'financeiro-lancamentos',
'documentos', 'documentos-templates', 'relatorios',
'perfil', 'plano', 'negocio', 'alterar-plano',
'perfil', 'plano', 'negocio', 'seguranca', 'alterar-plano',
...MELISSA_EMBED_KEYS
]);
// Aliases "bonitos" + INLINE_KEYS reconhecidos pelo MelissaConfiguracoes.
// Mantidos sincronizados com o ROUTE_ALIASES/INLINE_KEYS de lá.
const MELISSA_CONFIG_ALIASES = new Set([
'aparencia', 'seguranca', 'bloqueios',
'aparencia', 'bloqueios',
'fundo', 'relogio', 'cronometro'
]);
function isMelissaConfigRoute(slug) {
@@ -2237,6 +2239,11 @@ function onKeydown(e) {
@goto="abrirSecao"
/>
<MelissaSeguranca
v-if="layoutReady && secaoAberta === 'seguranca'"
@close="fecharSecao"
/>
<MelissaConfiguracoes
v-if="layoutReady && isMelissaConfigRoute(secaoAberta)"
:secao-rota="secaoAberta"
+963
View File
@@ -0,0 +1,963 @@
<script setup>
/*
* MelissaSeguranca — Pagina nativa Melissa pra "Seguranca".
*
* Substitui o embed cfg-seguranca que vivia dentro do MelissaConfiguracoes.
* Layout 2-col:
* - COL 1 (sidebar) — Card "Estado da conta" (email + sessao ativa) +
* Card "Boas praticas" (4 dicas)
* - COL 2 (main) — Card "Trocar senha" (3 password inputs + barra de
* forca + botoes Enviar link / Trocar senha)
*
* Logica espelhada do SecurityPage.vue (auth.signInWithPassword pra
* confirmar senha atual + auth.updateUser pra trocar + signOut global
* apos sucesso por seguranca; reset por email opcional).
*/
import { ref, computed, onMounted } from 'vue';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
// InputText/Password/Button: auto via PrimeVueResolver
const emit = defineEmits(['close']);
const toast = useToast();
// ── Breakpoints + drawer ───────────────────────────────────
const drawerOpen = ref(false);
const isMobile = ref(false);
let _mqMobile = null;
function _onMqMobileChange(e) {
isMobile.value = e.matches;
if (!e.matches) drawerOpen.value = false;
}
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
function fecharDrawer() { drawerOpen.value = false; }
// ── Estado ─────────────────────────────────────────────────
const userEmail = ref('');
const loading = ref(false);
const loadingReset = ref(false);
const done = ref(false);
const currentPassword = ref('');
const newPassword = ref('');
const confirmPassword = ref('');
// ── Validações de senha ────────────────────────────────────
function isStrongEnough(p) {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(p || '');
}
const strengthScore = computed(() => {
const p = newPassword.value;
if (!p) return 0;
let s = 0;
if (p.length >= 8) s++;
if (/[A-Z]/.test(p)) s++;
if (/[a-z]/.test(p)) s++;
if (/\d/.test(p)) s++;
return s;
});
const strengthLabel = computed(() => {
const labels = ['', 'Muito fraca', 'Fraca', 'Boa', 'Forte'];
return labels[strengthScore.value] || '';
});
const strengthColors = ['', '#ef4444', '#eab308', '#3b82f6', '#10b981'];
const strengthColor = computed(() => strengthColors[strengthScore.value] || 'transparent');
const strengthOk = computed(() => isStrongEnough(newPassword.value));
const matchOk = computed(() => !!confirmPassword.value && newPassword.value === confirmPassword.value);
const canSubmit = computed(
() => !!currentPassword.value && strengthOk.value && matchOk.value && !loading.value && !loadingReset.value
);
// ── Logout forte (apos trocar senha) ───────────────────────
async function hardLogout() {
try { await supabase.auth.signOut({ scope: 'global' }); }
catch { /* ignore */ }
try {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k?.startsWith('sb-') && k.includes('auth-token')) keys.push(k);
}
keys.forEach((k) => localStorage.removeItem(k));
} catch { /* ignore */ }
try { sessionStorage.removeItem('redirect_after_login'); }
catch { /* ignore */ }
window.location.replace('/auth/login');
}
function clearFields() {
currentPassword.value = '';
newPassword.value = '';
confirmPassword.value = '';
}
// ── Trocar senha ───────────────────────────────────────────
async function changePassword() {
loading.value = true;
try {
const { data: uData, error: uErr } = await supabase.auth.getUser();
if (uErr) throw uErr;
const email = uData?.user?.email;
if (!email) throw new Error('Sessão inválida. Faça login novamente.');
const { error: signError } = await supabase.auth.signInWithPassword({
email,
password: currentPassword.value
});
if (signError) throw new Error('Senha atual incorreta.');
const { error: upError } = await supabase.auth.updateUser({ password: newPassword.value });
if (upError) throw upError;
clearFields();
done.value = true;
toast.add({
severity: 'success',
summary: 'Senha atualizada',
detail: 'Por segurança, você será deslogado.',
life: 2500
});
setTimeout(() => hardLogout(), 2600);
} catch (e) {
toast.add({
severity: 'error',
summary: 'Erro',
detail: e?.message || 'Não foi possível trocar a senha.',
life: 4000
});
} finally {
loading.value = false;
}
}
// ── Reset por e-mail ───────────────────────────────────────
async function sendResetEmail() {
loadingReset.value = true;
try {
const { data: uData, error: uErr } = await supabase.auth.getUser();
if (uErr) throw uErr;
const email = uData?.user?.email;
if (!email) throw new Error('Sessão inválida. Faça login novamente.');
const redirectTo = `${window.location.origin}/auth/reset-password`;
const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo });
if (error) throw error;
toast.add({
severity: 'info',
summary: 'E-mail enviado',
detail: 'Verifique sua caixa de entrada para redefinir a senha.',
life: 5000
});
} catch (e) {
toast.add({
severity: 'error',
summary: 'Erro',
detail: e?.message || 'Falha ao enviar e-mail.',
life: 4500
});
} finally {
loadingReset.value = false;
}
}
// ── Lifecycle ──────────────────────────────────────────────
onMounted(async () => {
if (typeof window !== 'undefined' && window.matchMedia) {
_mqMobile = window.matchMedia('(max-width: 1023px)');
isMobile.value = _mqMobile.matches;
try { _mqMobile.addEventListener('change', _onMqMobileChange); }
catch { _mqMobile.addListener(_onMqMobileChange); }
}
try {
const { data } = await supabase.auth.getUser();
userEmail.value = data?.user?.email || '';
} catch { /* ignore */ }
});
</script>
<template>
<Transition name="mse-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="mse-mobile-drawer"
:class="{ 'is-open': drawerOpen }"
>
<div id="mse-mobile-drawer-target" class="mse-mobile-drawer__scroll" />
</div>
</Transition>
<Transition name="mse-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="mse-mobile-drawer__backdrop"
@click="fecharDrawer"
/>
</Transition>
<section class="mse-page">
<header class="mse-page__head">
<button
class="mse-menu-btn mse-menu-btn--mobile-only"
v-tooltip.bottom="'Estado & Boas práticas'"
@click="toggleDrawer"
>
<i class="pi pi-bars" />
<span>Menu</span>
</button>
<div class="mse-page__title">
<i class="pi pi-shield mse-page__title-icon" />
<span>Segurança</span>
</div>
<div class="mse-page__actions">
<button class="mse-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
<i class="pi pi-times" />
</button>
</div>
</header>
<div class="mse-subheader">
<i class="pi pi-info-circle mse-subheader__icon" />
<span class="mse-subheader__text">
Gerencie a senha e o acesso da sua conta. Ao trocar a senha,
você será deslogado de todos os dispositivos por segurança.
</span>
</div>
<div class="mse-body">
<Teleport to="#mse-mobile-drawer-target" :disabled="!isMobile">
<aside class="mse-side">
<div class="mse-side__scroll">
<!-- Card: Estado da conta -->
<div class="mse-w mse-w--side">
<div class="mse-w__head">
<div class="mse-w__icon"><i class="pi pi-id-card" /></div>
<div class="mse-w__title">
<div class="mse-w__title-text">Estado da conta</div>
<div class="mse-w__sub">Sessão e identificação</div>
</div>
</div>
<div class="mse-w__body">
<div class="mse-info">
<div class="mse-info__row">
<span class="mse-info__label">E-mail</span>
<span class="mse-info__value mse-info__value--mono">{{ userEmail || '—' }}</span>
</div>
<div class="mse-info__row">
<span class="mse-info__label">Sessão</span>
<span class="mse-tag-active">
<span class="mse-tag-active__dot" />
Ativa
</span>
</div>
</div>
<div class="mse-account-warn">
<i class="pi pi-exclamation-triangle" />
<span>Trocar a senha desconecta todos os dispositivos.</span>
</div>
</div>
</div>
<!-- Card: Boas práticas -->
<div class="mse-w mse-w--side">
<div class="mse-w__head">
<div class="mse-w__icon"><i class="pi pi-lightbulb" /></div>
<div class="mse-w__title">
<div class="mse-w__title-text">Boas práticas</div>
<div class="mse-w__sub">Como criar uma senha segura</div>
</div>
</div>
<div class="mse-w__body">
<ul class="mse-tips">
<li class="mse-tip">
<span class="mse-tip__bullet" style="background: #6366f1" />
<span>Use 8+ caracteres com maiúscula, minúscula e número.</span>
</li>
<li class="mse-tip">
<span class="mse-tip__bullet" style="background: #10b981" />
<span>Evite datas, nomes e sequências óbvias (1234, qwerty).</span>
</li>
<li class="mse-tip">
<span class="mse-tip__bullet" style="background: #d946ef" />
<span>Em computador compartilhado, encerre a sessão depois.</span>
</li>
<li class="mse-tip">
<span class="mse-tip__bullet" style="background: #f59e0b" />
<span>Não reutilize senhas de outros serviços.</span>
</li>
</ul>
</div>
</div>
</div>
</aside>
</Teleport>
<div class="mse-main">
<!-- Card: Trocar senha -->
<div class="mse-w">
<div class="mse-w__head">
<div class="mse-w__icon"><i class="pi pi-key" /></div>
<div class="mse-w__title">
<div class="mse-w__title-text">Trocar senha</div>
<div class="mse-w__sub">Confirme a senha atual e defina uma nova</div>
</div>
</div>
<div class="mse-w__body">
<!-- Estado: concluído -->
<div v-if="done" class="mse-done">
<div class="mse-done__icon">
<i class="pi pi-check" />
</div>
<div class="mse-done__title">Senha atualizada!</div>
<div class="mse-done__hint">Redirecionando para o login</div>
</div>
<!-- Estado: form -->
<template v-else>
<!-- Senha atual -->
<div class="mse-field">
<label for="mse_current" class="mse-field__label">Senha atual</label>
<Password
id="mse_current"
v-model="currentPassword"
placeholder="Digite sua senha atual"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full"
:disabled="loading || loadingReset"
/>
<small class="mse-hint">Necessária para confirmar que é você.</small>
</div>
<!-- Nova senha -->
<div class="mse-field">
<label for="mse_new" class="mse-field__label">Nova senha</label>
<Password
id="mse_new"
v-model="newPassword"
placeholder="Mínimo 8 caracteres"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full"
:disabled="loading || loadingReset"
/>
<div v-if="newPassword" class="mse-strength">
<div class="mse-strength__bars">
<div
v-for="i in 4"
:key="i"
class="mse-strength__bar"
:style="{
background: i <= strengthScore ? strengthColor : 'var(--m-border)'
}"
/>
</div>
<span class="mse-strength__label" :style="{ color: strengthColor }">
{{ strengthLabel }}
</span>
</div>
<small v-else class="mse-hint">
Critérios: 8+ caracteres, maiúscula, minúscula e número.
</small>
</div>
<!-- Confirmar senha -->
<div class="mse-field">
<label for="mse_confirm" class="mse-field__label">Confirmar nova senha</label>
<Password
id="mse_confirm"
v-model="confirmPassword"
placeholder="Repita a nova senha"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full"
:disabled="loading || loadingReset"
/>
<div
v-if="confirmPassword"
class="mse-match"
:class="matchOk ? 'is-ok' : 'is-warn'"
>
<i :class="matchOk ? 'pi pi-check' : 'pi pi-times'" />
{{ matchOk ? 'As senhas conferem.' : 'As senhas não conferem.' }}
</div>
</div>
<!-- Aviso -->
<div class="mse-warn">
<i class="pi pi-info-circle" />
<span>
Ao trocar sua senha, você será desconectado de todos os
dispositivos por segurança.
</span>
</div>
<!-- Ações -->
<div class="mse-actions">
<button
class="mse-btn"
:disabled="loading || loadingReset"
@click="sendResetEmail"
>
<i :class="loadingReset ? 'pi pi-spin pi-spinner' : 'pi pi-envelope'" />
<span>Enviar link por e-mail</span>
</button>
<button
class="mse-btn mse-btn--primary"
:disabled="!canSubmit"
@click="changePassword"
>
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
<span>Trocar senha</span>
</button>
</div>
</template>
</div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
/* ═══════ Page chrome ═══════ */
.mse-page {
position: absolute;
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) 6px;
z-index: 40;
display: flex;
flex-direction: column;
background: var(--m-bg-medium);
backdrop-filter: blur(32px) saturate(160%);
-webkit-backdrop-filter: blur(32px) saturate(160%);
border: 1px solid var(--m-border);
border-radius: 18px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
overflow: hidden;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
color: var(--m-text);
animation: mse-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
@keyframes mse-page-enter {
from { opacity: 0; transform: scale(0.985); }
to { opacity: 1; transform: scale(1); }
}
.mse-page__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--m-border);
flex-shrink: 0;
gap: 10px;
}
.mse-page__title {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
font-weight: 700;
color: var(--m-text);
}
.mse-page__title-icon { color: var(--p-primary-color); font-size: 1.05rem; }
.mse-page__actions { display: flex; align-items: center; gap: 6px; }
.mse-close {
width: 32px;
height: 32px;
display: grid;
place-items: center;
background: transparent;
border: 1px solid var(--m-border);
color: var(--m-text-muted);
border-radius: 8px;
cursor: pointer;
transition: background-color 120ms ease, color 120ms ease;
}
.mse-close:hover { background: var(--m-bg-soft-hover); color: var(--m-text); }
.mse-menu-btn {
display: none;
align-items: center;
gap: 6px;
padding: 7px 12px;
border-radius: 8px;
border: 1px solid var(--m-border);
background: var(--m-bg-soft);
color: var(--m-text);
cursor: pointer;
font-size: 0.82rem;
font-weight: 600;
flex-shrink: 0;
}
.mse-menu-btn:hover { background: var(--m-bg-soft-hover); }
.mse-subheader {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 18px;
background: var(--m-bg-soft);
border-bottom: 1px solid var(--m-border);
color: var(--m-text-muted);
font-size: 0.78rem;
flex-shrink: 0;
}
.mse-subheader__icon {
color: var(--p-primary-color);
font-size: 0.85rem;
margin-top: 2px;
flex-shrink: 0;
}
/* ═══════ Body 2-col ═══════ */
.mse-body {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
.mse-side {
width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--m-bg-soft);
border-right: 1px solid var(--m-border);
overflow: hidden;
}
.mse-side__scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mse-side__scroll::-webkit-scrollbar { width: 5px; }
.mse-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mse-main {
flex: 1;
min-width: 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mse-main::-webkit-scrollbar { width: 5px; }
.mse-main::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
/* ═══════ Card-base (estilo MelissaPerfil) ═══════ */
.mse-w {
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
}
.mse-w--side {
background: var(--m-bg-medium);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
}
.mse-w__head {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid var(--m-border);
}
.mse-w__icon {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 9px;
flex-shrink: 0;
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
color: var(--p-primary-color);
}
.mse-w__icon > i { font-size: 0.95rem; }
.mse-w__title { flex: 1; min-width: 0; }
.mse-w__title-text {
font-size: 0.92rem;
font-weight: 700;
color: var(--m-text);
line-height: 1.2;
}
.mse-w__sub {
font-size: 0.74rem;
color: var(--m-text-muted);
margin-top: 2px;
line-height: 1.3;
}
.mse-w__body {
padding: 14px;
display: flex;
flex-direction: column;
gap: 14px;
}
/* ═══════ Sidebar: Estado da conta ═══════ */
.mse-info {
display: flex;
flex-direction: column;
gap: 10px;
}
.mse-info__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.mse-info__label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--m-text-muted);
flex-shrink: 0;
}
.mse-info__value {
font-size: 0.78rem;
font-weight: 600;
color: var(--m-text);
text-align: right;
word-break: break-word;
}
.mse-info__value--mono {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.74rem;
user-select: all;
}
.mse-tag-active {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 10px;
border-radius: 999px;
background: rgba(16, 185, 129, 0.12);
border: 1px solid rgba(16, 185, 129, 0.30);
color: rgb(16, 185, 129);
font-size: 0.72rem;
font-weight: 700;
}
.mse-tag-active__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
animation: mse-pulse 1.6s ease-in-out infinite;
}
@keyframes mse-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
.mse-account-warn {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
border-radius: 9px;
background: rgba(217, 119, 6, 0.10);
border: 1px solid rgba(217, 119, 6, 0.30);
color: rgb(217, 119, 6);
font-size: 0.74rem;
line-height: 1.4;
}
.mse-account-warn > i { font-size: 0.85rem; margin-top: 2px; flex-shrink: 0; }
/* ═══════ Sidebar: Boas praticas ═══════ */
.mse-tips {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.mse-tip {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 0.78rem;
color: var(--m-text-muted);
line-height: 1.4;
}
.mse-tip__bullet {
width: 8px;
height: 8px;
border-radius: 50%;
margin-top: 6px;
flex-shrink: 0;
}
/* ═══════ Main: Trocar senha ═══════ */
.mse-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.mse-field__label {
font-size: 0.82rem;
font-weight: 700;
color: var(--m-text);
}
.mse-hint, .mse-err {
display: block;
font-size: 0.72rem;
line-height: 1.3;
color: var(--m-text-muted);
}
.mse-strength {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 4px;
}
.mse-strength__bars {
display: flex;
gap: 4px;
}
.mse-strength__bar {
flex: 1;
height: 4px;
border-radius: 999px;
transition: background-color 200ms ease;
}
.mse-strength__label {
font-size: 0.72rem;
font-weight: 700;
}
.mse-match {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
font-weight: 600;
margin-top: 2px;
}
.mse-match.is-ok { color: rgb(16, 185, 129); }
.mse-match.is-warn { color: rgb(217, 119, 6); }
.mse-warn {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
border-radius: 9px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
color: var(--m-text-muted);
font-size: 0.78rem;
line-height: 1.4;
}
.mse-warn > i { font-size: 0.92rem; margin-top: 2px; flex-shrink: 0; color: var(--p-primary-color); }
.mse-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 4px;
}
.mse-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 14px;
border-radius: 8px;
border: 1px solid var(--m-border);
background: var(--m-bg-medium);
color: var(--m-text);
cursor: pointer;
font-size: 0.82rem;
font-weight: 600;
font-family: inherit;
transition: background-color 120ms ease, opacity 120ms ease;
}
.mse-btn:hover { background: var(--m-bg-soft-hover); }
.mse-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.mse-btn--primary {
background: var(--p-primary-color);
border-color: var(--p-primary-color);
color: var(--p-primary-contrast-color, #fff);
margin-left: auto;
}
.mse-btn--primary:hover {
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
}
.mse-done {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
padding: 32px 12px;
}
.mse-done__icon {
width: 64px;
height: 64px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(16, 185, 129, 0.12);
border: 1px solid rgba(16, 185, 129, 0.30);
color: rgb(16, 185, 129);
font-size: 2rem;
}
.mse-done__title {
font-size: 1rem;
font-weight: 700;
color: var(--m-text);
}
.mse-done__hint {
font-size: 0.82rem;
color: var(--m-text-muted);
}
/* ═══════ Mobile drawer ═══════ */
.mse-mobile-drawer {
position: fixed;
top: 0; left: 0;
height: 100vh;
height: 100dvh;
width: min(360px, 88vw);
z-index: 80;
background: var(--m-bg-medium);
backdrop-filter: blur(28px) saturate(160%);
-webkit-backdrop-filter: blur(28px) saturate(160%);
border-right: 1px solid var(--m-border);
transform: translateX(-100%);
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--m-text);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
display: flex;
flex-direction: column;
}
.mse-mobile-drawer.is-open { transform: translateX(0); }
.mse-mobile-drawer__scroll {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mse-mobile-drawer__scroll .mse-side {
flex: 1;
min-height: 0;
width: 100%;
overflow: hidden;
background: transparent;
border-right: none;
display: flex;
flex-direction: column;
}
.mse-mobile-drawer__scroll .mse-side__scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mse-mobile-drawer__scroll .mse-side__scroll::-webkit-scrollbar { width: 5px; }
.mse-mobile-drawer__scroll .mse-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mse-mobile-drawer__scroll .mse-w--side {
margin: 0;
flex-shrink: 0;
}
.mse-mobile-drawer__backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 79;
}
.mse-drawer-fade-enter-active,
.mse-drawer-fade-leave-active { transition: opacity 200ms ease; }
.mse-drawer-fade-enter-from,
.mse-drawer-fade-leave-to { opacity: 0; }
/* ═══════ Desktop (>=1024px): cards min-h 300 + body scroll ═══════ */
@media (min-width: 1024px) {
.mse-side > .mse-side__scroll > .mse-w--side {
min-height: 300px;
max-height: 100%;
display: flex;
flex-direction: column;
}
.mse-side .mse-w--side > .mse-w__body {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mse-side .mse-w--side > .mse-w__body::-webkit-scrollbar { width: 5px; }
.mse-side .mse-w--side > .mse-w__body::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
}
/* ═══════ Mobile (<1024px) ═══════ */
@media (max-width: 1023px) {
.mse-body { flex-direction: column; padding: 0; }
.mse-side { display: none; }
.mse-main { width: 100%; padding: 8px; }
.mse-main .mse-w {
height: auto;
flex: 0 0 auto;
align-self: stretch;
}
.mse-page__title > span:first-of-type { display: none; }
.mse-page__title-icon { display: none; }
.mse-menu-btn--mobile-only { display: inline-flex; }
.mse-actions { flex-direction: column; }
.mse-btn--primary { margin-left: 0; }
}
</style>