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:
@@ -39,7 +39,6 @@ const emit = defineEmits(['close']);
|
|||||||
// existir, senão a key como-é.
|
// existir, senão a key como-é.
|
||||||
const ROUTE_ALIASES = {
|
const ROUTE_ALIASES = {
|
||||||
aparencia: 'aparencia',
|
aparencia: 'aparencia',
|
||||||
seguranca: 'cfg-seguranca',
|
|
||||||
bloqueios: 'cfg-bloqueios'
|
bloqueios: 'cfg-bloqueios'
|
||||||
};
|
};
|
||||||
// URLs antigas (/melissa/fundo, /melissa/relogio, /melissa/cronometro)
|
// URLs antigas (/melissa/fundo, /melissa/relogio, /melissa/cronometro)
|
||||||
@@ -96,10 +95,10 @@ const COMPONENT_MAP = {
|
|||||||
'cfg-recursos-extras': defineAsyncComponent(() => import('@/layout/configuracoes/ConfiguracoesRecursosExtrasPage.vue')),
|
'cfg-recursos-extras': defineAsyncComponent(() => import('@/layout/configuracoes/ConfiguracoesRecursosExtrasPage.vue')),
|
||||||
'cfg-auditoria': defineAsyncComponent(() => import('@/layout/configuracoes/AuditoriaPage.vue')),
|
'cfg-auditoria': defineAsyncComponent(() => import('@/layout/configuracoes/AuditoriaPage.vue')),
|
||||||
// Conta (páginas pessoais que vivem em /account/*)
|
// Conta (páginas pessoais que vivem em /account/*)
|
||||||
// 'cfg-perfil' removido — virou pagina nativa MelissaPerfil em /melissa/perfil
|
// 'cfg-perfil' removido — virou pagina nativa MelissaPerfil em /melissa/perfil
|
||||||
// 'cfg-plano' removido — virou pagina nativa MelissaPlano em /melissa/plano
|
// 'cfg-plano' removido — virou pagina nativa MelissaPlano em /melissa/plano
|
||||||
// 'cfg-negocio' removido — virou pagina nativa MelissaNegocio em /melissa/negocio
|
// '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).
|
// 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: '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' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
// Grupo "Conta" inteiro saiu — todos os 4 items (Perfil/Plano/
|
||||||
key: 'conta',
|
// Negocio/Seguranca) viraram paginas nativas no /melissa/<slug>.
|
||||||
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' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'agenda',
|
key: 'agenda',
|
||||||
label: 'Agenda',
|
label: 'Agenda',
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import MelissaPerfil from './MelissaPerfil.vue';
|
|||||||
import MelissaPlano from './MelissaPlano.vue';
|
import MelissaPlano from './MelissaPlano.vue';
|
||||||
import MelissaNegocio from './MelissaNegocio.vue';
|
import MelissaNegocio from './MelissaNegocio.vue';
|
||||||
import MelissaAlterarPlano from './MelissaAlterarPlano.vue';
|
import MelissaAlterarPlano from './MelissaAlterarPlano.vue';
|
||||||
|
import MelissaSeguranca from './MelissaSeguranca.vue';
|
||||||
import MelissaEmbed from './MelissaEmbed.vue';
|
import MelissaEmbed from './MelissaEmbed.vue';
|
||||||
import MelissaCadastrosRecebidos from './MelissaCadastrosRecebidos.vue';
|
import MelissaCadastrosRecebidos from './MelissaCadastrosRecebidos.vue';
|
||||||
import MelissaAgendamentosRecebidos from './MelissaAgendamentosRecebidos.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.' },
|
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
|
// 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.' },
|
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
|
// 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.' },
|
'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)
|
// 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': { 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.' },
|
'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',
|
'tags', 'grupos', 'cadastros-recebidos', 'medicos', 'agendamentos-recebidos',
|
||||||
'link-externo', 'notificacoes', 'financeiro', 'financeiro-lancamentos',
|
'link-externo', 'notificacoes', 'financeiro', 'financeiro-lancamentos',
|
||||||
'documentos', 'documentos-templates', 'relatorios',
|
'documentos', 'documentos-templates', 'relatorios',
|
||||||
'perfil', 'plano', 'negocio', 'alterar-plano',
|
'perfil', 'plano', 'negocio', 'seguranca', 'alterar-plano',
|
||||||
...MELISSA_EMBED_KEYS
|
...MELISSA_EMBED_KEYS
|
||||||
]);
|
]);
|
||||||
// Aliases "bonitos" + INLINE_KEYS reconhecidos pelo MelissaConfiguracoes.
|
// Aliases "bonitos" + INLINE_KEYS reconhecidos pelo MelissaConfiguracoes.
|
||||||
// Mantidos sincronizados com o ROUTE_ALIASES/INLINE_KEYS de lá.
|
// Mantidos sincronizados com o ROUTE_ALIASES/INLINE_KEYS de lá.
|
||||||
const MELISSA_CONFIG_ALIASES = new Set([
|
const MELISSA_CONFIG_ALIASES = new Set([
|
||||||
'aparencia', 'seguranca', 'bloqueios',
|
'aparencia', 'bloqueios',
|
||||||
'fundo', 'relogio', 'cronometro'
|
'fundo', 'relogio', 'cronometro'
|
||||||
]);
|
]);
|
||||||
function isMelissaConfigRoute(slug) {
|
function isMelissaConfigRoute(slug) {
|
||||||
@@ -2237,6 +2239,11 @@ function onKeydown(e) {
|
|||||||
@goto="abrirSecao"
|
@goto="abrirSecao"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<MelissaSeguranca
|
||||||
|
v-if="layoutReady && secaoAberta === 'seguranca'"
|
||||||
|
@close="fecharSecao"
|
||||||
|
/>
|
||||||
|
|
||||||
<MelissaConfiguracoes
|
<MelissaConfiguracoes
|
||||||
v-if="layoutReady && isMelissaConfigRoute(secaoAberta)"
|
v-if="layoutReady && isMelissaConfigRoute(secaoAberta)"
|
||||||
:secao-rota="secaoAberta"
|
:secao-rota="secaoAberta"
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user