989c5330f8
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>
988 lines
33 KiB
Vue
988 lines
33 KiB
Vue
<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:
|
||
- 768px–1012px : full-width (right: 6px) — viewports tablet/laptop
|
||
pequeno onde 1000px nao caberia
|
||
- 1012px–2012px: 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>
|