Files
agenciapsilmno/src/layout/melissa/MelissaSeguranca.vue
T
Leonardo 989c5330f8 MelissaLayout: sidebar global de configs em qualquer rota de config
Antes cada pagina nativa de config tinha seu proprio chrome 2-col, e
quando o usuario navegava entre Perfil/Plano/Negocio/Seguranca/Agenda
Config/Bloqueios/Agendador/Pagamento, perdia o contexto do menu.

Agora:
- Catalogo unico em composables/melissaConfigGrupos.js (MELISSA_CONFIG_
  GRUPOS + isMelissaConfigSlug helper)
- MelissaConfigSidebar.vue componente standalone com accordion +
  navegacao via router.push + destaque do item ativo
- MelissaLayout renderiza `<MelissaConfigSidebar>` em qualquer slug
  que esteja em MELISSA_CONFIG_GRUPOS (computed showConfigSidebar)
- CSS var --m-config-aside-left no .win11-root: 296px quando sidebar
  visivel, 6px caso contrario
- Todas as 9 paginas nativas (Perfil, Plano, AlterarPlano, Negocio,
  Seguranca, Bloqueios, AgendaConfig, Agendador, Pagamento) +
  MelissaConfiguracoes ajustam left do inset usando a var

Sidebar tem entrada animada (lift + slide) e usa o pattern do .mcfg-
accordion (head com icone primary + label + desc 2-linhas + badge;
items com hover/active color-mix primary 12-16%).

Proximo passo: limpar o aside redundante interno do MelissaConfiguracoes
+ ajustar MelissaSeguranca pra considerar o aside no min-width 1000.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:50:03 -03:00

988 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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: 767px)');
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) var(--m-config-aside-left, 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 (>=768px) — breakpoint menor que as outras Melissa
pages (1024px) porque essa pagina e enxuta (so 1 form pequeno) e
cabe bem em tablet portrait. ═══════ */
@media (min-width: 768px) {
/* Largura adaptativa, alinhada a esquerda:
- 768px1012px : full-width (right: 6px) — viewports tablet/laptop
pequeno onde 1000px nao caberia
- 1012px2012px: width = 1000px fixo (right cresce com viewport)
- >= 2012px : width = ~50% do viewport (right: 50%)
Formula: max(6px, min(50%, calc(100% - 1006px))) */
.mse-page {
right: max(6px, min(50%, calc(100% - 1006px)));
}
/* Sidebar: cards min-h 300 + max-h 100% + body scroll */
.mse-side > .mse-side__scroll > .mse-w--side {
min-height: 300px;
max-height: 100%;
display: flex;
flex-direction: column;
}
/* Main: card "Trocar senha" sem min-height (altura por conteudo)
mas cap-ado em max-height: 100% do main pra nao passar do viewport;
body com overflow-y: auto pra scroll interno se conteudo for longo. */
.mse-main > .mse-w {
max-height: 100%;
display: flex;
flex-direction: column;
}
.mse-side .mse-w--side > .mse-w__body,
.mse-main > .mse-w > .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,
.mse-main > .mse-w > .mse-w__body::-webkit-scrollbar { width: 5px; }
.mse-side .mse-w--side > .mse-w__body::-webkit-scrollbar-thumb,
.mse-main > .mse-w > .mse-w__body::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
}
/* ═══════ Mobile (<768px) ═══════ */
@media (max-width: 767px) {
.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>