Files
agenciapsilmno/src/layout/melissa/MelissaPerfil.vue
T
Leonardo dee89ccd84 registro profissional: campo livre quando tipo='outro'
Quando o profissional seleciona "Outro" no Tipo de registro, agora
aparece um campo adicional pra informar o nome do conselho/instituicao
livre (ex: APM, ABRAP, conselhos nao-listados).

Migration 20260521000009 adiciona profiles.professional_registration_
type_other (text livre). Aplicada e marcada no _db_migrations.

ProfilePage e MelissaPerfil:
- form.professional_registration_type_other no reactive
- SELECT/UPDATE inclui a nova coluna
- UI condicional: campo aparece SOMENTE quando type === 'outro'
- Preview ao vivo usa type_other no lugar de 'outro' quando aplicavel
- Save limpa type_other automaticamente quando troca pra outro tipo

DocumentGenerate.service.loadTherapistData puxa type_other da query.
Quando profile.type='outro', terapeuta_registro_tipo recebe o valor
livre (ex: 'APM 12345/SP' em vez de 'outro 12345/SP'). terapeuta_crp
(legacy compat) continua so preenchido quando type RAW = 'CRP'.

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

2258 lines
89 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>
/*
* MelissaPerfil — Pagina nativa Melissa pra "Meu Perfil".
*
* Substitui o embed cfg-perfil que vivia dentro do MelissaConfiguracoes.
* Layout 2-col:
* - COL 1 (sidebar) — Gamificação (nivel + XP + badges + dicas) + Avatar
* (upload/remover) + footer Sair da conta
* - COL 2 (main) — Identidade + Contato + Bio + Sites e Redes
*
* Aparência (tema/cores) e Layout Variant ficaram fora — Aparencia ja
* vive no MelissaConfiguracoes "Layout Melissa" e Layout Variant nao
* faz sentido dentro do shell Melissa. Trocar senha vai pro cfg-seguranca.
*
* Logica de load/save espelhada do ProfilePage.vue (mesmas tabelas
* profiles + user_settings + auth.user_metadata, mesmo bucket avatars).
*/
import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import { useRouter } from 'vue-router';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import { useLayout } from '@/layout/composables/layout';
import MelissaConfigList from './MelissaConfigList.vue';
// InputText/Select/Textarea/InputMask/Skeleton/Tag/Button: auto via PrimeVueResolver
const emit = defineEmits(['close']);
const toast = useToast();
const confirm = useConfirm();
const router = useRouter();
const tenantStore = useTenantStore();
const { layoutConfig, setVariant, isDarkTheme, toggleDarkMode } = useLayout();
// Opções do CHECK constraint da migration 20260521000003 (CFP #5)
const REGISTRATION_TYPE_OPTIONS = [
{ value: '', label: '— Não informado —' },
{ value: 'CRP', label: 'CRP — Psicólogo(a)' },
{ value: 'CRM', label: 'CRM — Médico(a)' },
{ value: 'CRFa', label: 'CRFa — Fonoaudiólogo(a)' },
{ value: 'CREFITO', label: 'CREFITO — Fisioterapeuta / T.O.' },
{ value: 'CRESS', label: 'CRESS — Assistente Social' },
{ value: 'CRN', label: 'CRN — Nutricionista' },
{ value: 'RMS', label: 'RMS — Residência Multiprofissional' },
{ value: 'outro', label: 'Outro' }
];
const UF_OPTIONS = [
'AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB',
'PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO'
].map(uf => ({ value: uf, label: uf }));
function goSeguranca() {
// Página nativa Melissa — não vaza pra layout clássico
router.push('/melissa/seguranca');
}
// Troca de layout variant (classic/rail/melissa). Confirma + persiste +
// hard reload — sair do shell Melissa requer reload pq AppLayout não tem
// branch pra essa rota; quem renderiza Melissa é a rota /melissa separada.
const variantSwitchOpen = ref(false);
async function switchToVariant(v) {
if (!['classic', 'rail', 'melissa'].includes(v)) return;
if (layoutConfig.variant === v) return;
if (variantSwitchOpen.value) return;
variantSwitchOpen.value = true;
const labels = { classic: 'Clássico', rail: 'Rail', melissa: 'Melissa' };
confirm.require({
header: `Trocar para o layout ${labels[v]}`,
message: 'A página será recarregada para aplicar o novo layout. Confirma?',
icon: 'pi pi-th-large',
acceptLabel: 'Trocar e recarregar',
rejectLabel: 'Cancelar',
accept: async () => {
try {
setVariant(v);
if (userId.value) {
const { error } = await supabase
.from('user_settings')
.upsert(
{
user_id: userId.value,
layout_variant: v,
updated_at: new Date().toISOString()
},
{ onConflict: 'user_id' }
);
if (error) {
const msg = String(error.message || '');
const tolerant = /does not exist/i.test(msg) || /permission denied/i.test(msg) || /violates row-level security/i.test(msg);
if (!tolerant) throw error;
}
}
toast.add({ severity: 'info', summary: `Aplicando ${labels[v]}`, detail: 'Recarregando…', life: 1500 });
window.location.assign('/');
} catch (e) {
variantSwitchOpen.value = false;
toast.add({ severity: 'error', summary: 'Erro ao trocar layout', detail: e?.message || 'Tente novamente.', life: 4000 });
}
},
reject: () => { variantSwitchOpen.value = false; },
onHide: () => { variantSwitchOpen.value = false; }
});
}
const AVATAR_BUCKET = 'avatars';
// ── 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; }
// Toggle entre cards contextuais (default) e lista de configs (alterna
// inline na .mpr-side via v-if — sem overlay/popover, zero overhead).
const cfgOpen = ref(false);
function toggleCfg() { cfgOpen.value = !cfgOpen.value; }
function fecharCfg() { cfgOpen.value = false; }
// ── Estado ─────────────────────────────────────────────────
const loading = ref(true);
const saving = ref(false);
const dirty = ref(false);
const silentApplying = ref(true);
const userId = ref('');
const userEmail = ref('');
const userRole = ref(null);
const fieldErrors = reactive({
full_name: '',
nickname: '',
phone: ''
});
function clearErr(field) { fieldErrors[field] = ''; }
function markDirty() {
if (silentApplying.value) return;
dirty.value = true;
}
const form = reactive({
full_name: '',
nickname: '',
work_description: '',
work_description_other: '',
avatar_url: '',
bio: '',
phone: '',
site_url: '',
social_instagram: '',
social_youtube: '',
social_facebook: '',
social_x: '',
// Registro profissional (CFP #5 — exigido pra emissão de recibos/laudos)
professional_registration_type: '',
professional_registration_type_other: '',
professional_registration_number: '',
professional_registration_uf: ''
});
const customSocials = ref([]);
function addCustomSocial() {
customSocials.value.push({ name: '', url: '' });
markDirty();
}
function removeCustomSocial(idx) {
customSocials.value.splice(idx, 1);
markDirty();
}
const ui = reactive({
avatarPreview: '',
avatarFile: null,
avatarFilePreviewUrl: ''
});
const fileInput = ref(null);
const workDescriptionOptions = [
{ label: 'Psicólogo(a) Clínico(a)', value: 'psicologo_clinico' },
{ label: 'Psicanalista', value: 'psicanalista' },
{ label: 'Psiquiatra', value: 'psiquiatra' },
{ label: 'Psicoterapeuta', value: 'psicoterapeuta' },
{ label: 'Neuropsicólogo(a)', value: 'neuropsicologo' },
{ label: 'Psicólogo(a) Organizacional', value: 'psicologo_organizacional' },
{ label: 'Psicólogo(a) Escolar / Educacional', value: 'psicologo_escolar' },
{ label: 'Psicólogo(a) Hospitalar', value: 'psicologo_hospitalar' },
{ label: 'Psicólogo(a) Jurídico(a)', value: 'psicologo_juridico' },
{ label: 'Coach / Mentor(a)', value: 'coach_mentor' },
{ label: 'Terapeuta Holístico(a)', value: 'terapeuta_holistico' },
{ label: 'Outro', value: 'outro' }
];
// ── Iniciais (avatar fallback) ──────────────────────────────
const initials = computed(() => {
const name = form.full_name || userEmail.value || '';
const parts = String(name).trim().split(/\s+/).filter(Boolean);
const a = parts[0]?.[0] || 'U';
const b = parts.length > 1 ? parts[parts.length - 1][0] : '';
return (a + b).toUpperCase();
});
// ── Gamificação / Progresso ────────────────────────────────
const profileFields = computed(() => [
{ key: 'full_name', filled: !!form.full_name?.trim(), icon: 'pi pi-user', text: 'Preencha seu nome completo' },
{ key: 'nickname', filled: !!form.nickname?.trim(), icon: 'pi pi-comment', text: 'Escolha um nome de exibição' },
{ key: 'work_description', filled: !!form.work_description?.trim(), icon: 'pi pi-briefcase', text: 'Descreva seu trabalho' },
{ key: 'avatar', filled: !!(form.avatar_url?.trim() || ui.avatarFile), icon: 'pi pi-image', text: 'Adicione uma foto' },
{ key: 'bio', filled: !!form.bio?.trim(), icon: 'pi pi-pencil', text: 'Complete sua bio' },
{ key: 'phone', filled: !!form.phone?.trim()?.replace(/[^0-9]/g, ''), icon: 'pi pi-whatsapp', text: 'Informe seu WhatsApp' },
{ key: 'social', filled: !!(form.social_instagram || form.social_x || form.site_url || form.social_youtube || form.social_facebook || customSocials.value.length), icon: 'pi pi-share-alt', text: 'Adicione uma rede social' }
]);
const profileProgress = computed(() => {
const filled = profileFields.value.filter((f) => f.filled).length;
return Math.round((filled / profileFields.value.length) * 100);
});
const progressSuggestions = computed(() => profileFields.value.filter((f) => !f.filled));
const progressColor = computed(() => {
if (profileProgress.value >= 80) return '#10b981';
if (profileProgress.value >= 50) return '#f59e0b';
return '#ef4444';
});
const gameLevels = [
{ min: 0, max: 14, label: 'Iniciante', icon: '🌱', color: '#94a3b8' },
{ min: 15, max: 28, label: 'Aprendiz', icon: '🌿', color: '#60a5fa' },
{ min: 29, max: 42, label: 'Praticante', icon: '⚡', color: '#f59e0b' },
{ min: 43, max: 57, label: 'Avançado', icon: '🔥', color: '#f97316' },
{ min: 58, max: 71, label: 'Expert', icon: '💎', color: '#a78bfa' },
{ min: 72, max: 85, label: 'Mestre', icon: '🏆', color: '#10b981' },
{ min: 86, max: 100, label: 'Lendário', icon: '🌟', color: '#eab308' }
];
const currentLevel = computed(() =>
gameLevels.find((l) => profileProgress.value >= l.min && profileProgress.value <= l.max) || gameLevels[0]
);
const nextLevel = computed(() => {
const i = gameLevels.indexOf(currentLevel.value);
return i < gameLevels.length - 1 ? gameLevels[i + 1] : null;
});
const xpToNext = computed(() => (nextLevel.value ? nextLevel.value.min - profileProgress.value : 0));
const badges = computed(() => [
{ key: 'name', earned: !!form.full_name?.trim(), icon: '👤', label: 'Identificado' },
{ key: 'nick', earned: !!form.nickname?.trim(), icon: '✏️', label: 'Apelido' },
{ key: 'work', earned: !!form.work_description?.trim(), icon: '💼', label: 'Profissional' },
{ key: 'photo', earned: !!(form.avatar_url?.trim() || ui.avatarFile), icon: '📷', label: 'Fotogênico' },
{ key: 'bio', earned: !!form.bio?.trim(), icon: '📝', label: 'Eloquente' },
{ key: 'phone', earned: !!form.phone?.trim()?.replace(/[^0-9]/g, ''), icon: '📱', label: 'Conectado' },
{ key: 'social', earned: !!(form.social_instagram || form.social_x || form.site_url || form.social_youtube || form.social_facebook || customSocials.value.length), icon: '🌐', label: 'Social' }
]);
// ── Validação ──────────────────────────────────────────────
function validateRequired() {
const nameParts = form.full_name?.trim().split(/\s+/).filter(Boolean) || [];
fieldErrors.full_name =
nameParts.length === 0 ? 'Nome completo é obrigatório.'
: nameParts.length < 2 ? 'Informe seu nome e sobrenome.'
: '';
fieldErrors.nickname = form.nickname?.trim() ? '' : 'Nome de exibição é obrigatório.';
const digits = form.phone?.replace(/[^0-9]/g, '') || '';
fieldErrors.phone = digits.length >= 10 ? '' : 'WhatsApp é obrigatório.';
return !fieldErrors.full_name && !fieldErrors.nickname && !fieldErrors.phone;
}
// ── Avatar ─────────────────────────────────────────────────
function pickAvatar() { fileInput.value?.click(); }
function clearAvatarFile() {
ui.avatarFile = null;
if (ui.avatarFilePreviewUrl) {
try { URL.revokeObjectURL(ui.avatarFilePreviewUrl); } catch { /* ignore */ }
}
ui.avatarFilePreviewUrl = '';
if (fileInput.value) fileInput.value.value = '';
}
function onAvatarFileSelected(ev) {
const file = ev?.target?.files?.[0];
if (!file) return;
if (!file.type?.startsWith('image/')) {
toast.add({ severity: 'warn', summary: 'Arquivo inválido', detail: 'Escolha uma imagem (PNG/JPG/WebP).', life: 3500 });
clearAvatarFile();
return;
}
if (file.size > 5 * 1024 * 1024) {
toast.add({ severity: 'warn', summary: 'Arquivo grande', detail: 'Máximo recomendado: 5MB.', life: 3500 });
clearAvatarFile();
return;
}
ui.avatarFile = file;
if (ui.avatarFilePreviewUrl) {
try { URL.revokeObjectURL(ui.avatarFilePreviewUrl); } catch { /* ignore */ }
}
ui.avatarFilePreviewUrl = URL.createObjectURL(file);
ui.avatarPreview = ui.avatarFilePreviewUrl;
markDirty();
}
function removeAvatar() {
form.avatar_url = '';
ui.avatarPreview = '';
clearAvatarFile();
markDirty();
}
function extFromMime(mime) {
if (!mime) return 'png';
if (mime.includes('jpeg')) return 'jpg';
if (mime.includes('png')) return 'png';
if (mime.includes('webp')) return 'webp';
return 'png';
}
async function uploadAvatarIfNeeded() {
if (!ui.avatarFile) return null;
if (!userId.value) throw new Error('Sessão inválida para upload.');
const file = ui.avatarFile;
const ext = extFromMime(file.type);
const path = `${userId.value}/avatar-${Date.now()}.${ext}`;
const { error: upErr } = await supabase.storage
.from(AVATAR_BUCKET)
.upload(path, file, { upsert: true, contentType: file.type });
if (upErr) throw upErr;
const { data } = supabase.storage.from(AVATAR_BUCKET).getPublicUrl(path);
const url = data?.publicUrl;
if (!url) throw new Error('Upload ok, mas não consegui obter a URL pública.');
return url;
}
// ── Carregar / Salvar ──────────────────────────────────────
async function ensureProfileExists(uid) {
const { data: prof, error: selErr } = await supabase
.from('profiles').select('id, role').eq('id', uid).maybeSingle();
if (selErr) throw selErr;
if (prof?.id) return prof;
const { data: created, error: insErr } = await supabase
.from('profiles').insert({ id: uid, role: 'portal_user' }).select('id, role').single();
if (insErr) throw insErr;
return created;
}
async function loadProfile() {
silentApplying.value = true;
loading.value = true;
try {
const { data: u, error: uErr } = await supabase.auth.getUser();
if (uErr) throw uErr;
const user = u?.user;
if (!user) throw new Error('Você precisa estar logado.');
userId.value = user.id;
userEmail.value = user.email || '';
await ensureProfileExists(user.id);
const meta = user.user_metadata || {};
form.full_name = meta.full_name || '';
form.avatar_url = meta.avatar_url || '';
ui.avatarPreview = form.avatar_url;
const { data: prof, error: pErr } = await supabase
.from('profiles')
.select(
'role, full_name, avatar_url, phone, bio, nickname, work_description, work_description_other, site_url, social_instagram, social_youtube, social_facebook, social_x, social_custom, professional_registration_type, professional_registration_type_other, professional_registration_number, professional_registration_uf'
)
.eq('id', user.id)
.maybeSingle();
if (!pErr && prof) {
userRole.value = prof.role || null;
form.full_name = prof.full_name ?? form.full_name;
form.avatar_url = prof.avatar_url ?? form.avatar_url;
form.phone = prof.phone ?? '';
form.bio = prof.bio ?? '';
form.nickname = prof.nickname ?? '';
form.work_description = prof.work_description ?? '';
form.work_description_other = prof.work_description_other ?? '';
form.site_url = prof.site_url ?? '';
form.social_instagram = prof.social_instagram ?? '';
form.social_youtube = prof.social_youtube ?? '';
form.social_facebook = prof.social_facebook ?? '';
form.social_x = prof.social_x ?? '';
form.professional_registration_type = prof.professional_registration_type ?? '';
form.professional_registration_type_other = prof.professional_registration_type_other ?? '';
form.professional_registration_number = prof.professional_registration_number ?? '';
form.professional_registration_uf = prof.professional_registration_uf ?? '';
customSocials.value = Array.isArray(prof.social_custom) ? prof.social_custom : [];
ui.avatarPreview = form.avatar_url;
}
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não consegui carregar o perfil.', life: 6000 });
} finally {
loading.value = false;
silentApplying.value = false;
dirty.value = false;
}
}
async function saveAll() {
if (!validateRequired()) {
toast.add({
severity: 'warn',
summary: 'Campos obrigatórios',
detail: 'Preencha nome completo, nome de exibição e WhatsApp antes de salvar.',
life: 4000
});
return;
}
saving.value = true;
try {
if (ui.avatarFile) {
try {
const uploadedUrl = await uploadAvatarIfNeeded();
if (uploadedUrl) {
form.avatar_url = uploadedUrl;
ui.avatarPreview = uploadedUrl;
}
} catch (e) {
toast.add({
severity: 'warn',
summary: 'Avatar não subiu',
detail: `Não consegui enviar o arquivo (bucket "${AVATAR_BUCKET}"). (${e?.message || 'erro'})`,
life: 6500
});
}
}
const metaPayload = {
full_name: String(form.full_name || '').trim(),
avatar_url: String(form.avatar_url || '').trim() || null
};
const { error: upErr } = await supabase.auth.updateUser({ data: metaPayload });
if (upErr) throw upErr;
await ensureProfileExists(userId.value);
const profilePayload = {
full_name: metaPayload.full_name,
avatar_url: metaPayload.avatar_url,
phone: String(form.phone || '').trim() || null,
bio: String(form.bio || '').trim() || null,
nickname: String(form.nickname || '').trim() || null,
work_description: String(form.work_description || '').trim() || null,
work_description_other:
form.work_description === 'outro'
? String(form.work_description_other || '').trim() || null
: null,
site_url: String(form.site_url || '').trim() || null,
social_instagram: String(form.social_instagram || '').trim() || null,
social_youtube: String(form.social_youtube || '').trim() || null,
social_facebook: String(form.social_facebook || '').trim() || null,
social_x: String(form.social_x || '').trim() || null,
social_custom: customSocials.value.filter((s) => s.name || s.url),
// Registro profissional (CFP) — null se vazio
professional_registration_type: String(form.professional_registration_type || '').trim() || null,
// type_other só preenchido quando type === 'outro' (limpa quando muda)
professional_registration_type_other:
form.professional_registration_type === 'outro'
? (String(form.professional_registration_type_other || '').trim() || null)
: null,
professional_registration_number: String(form.professional_registration_number || '').trim() || null,
professional_registration_uf: String(form.professional_registration_uf || '').trim() || null
};
const { data: updatedProfile, error: pErr2 } = await supabase
.from('profiles')
.update(profilePayload)
.eq('id', userId.value)
.select('id')
.single();
if (pErr2) {
const msg = String(pErr2.message || '');
const tolerant = /does not exist/i.test(msg) || /column .* does not exist/i.test(msg)
|| /relation .* does not exist/i.test(msg) || /permission denied/i.test(msg)
|| /violates row-level security/i.test(msg);
if (!tolerant) throw pErr2;
}
if (!updatedProfile) throw new Error('Perfil não encontrado para atualização.');
clearAvatarFile();
dirty.value = false;
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Perfil atualizado.', life: 2500 });
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro ao salvar', detail: e?.message || 'Não consegui salvar.', life: 6000 });
} finally {
saving.value = false;
}
}
// ── Ancoras: badge/dica leva pra sessao do form ────────────
const SECTION_BY_FIELD = {
full_name: 'identidade',
nickname: 'identidade',
work_description: 'identidade',
name: 'identidade',
nick: 'identidade',
work: 'identidade',
avatar: 'avatar',
photo: 'avatar',
bio: 'bio',
phone: 'contato',
social: 'redes'
};
function scrollToSection(field) {
const sec = SECTION_BY_FIELD[field];
if (!sec) return;
nextTick(() => {
const target = document.getElementById('mpr-sec-' + sec);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
// ── Sair da conta ──────────────────────────────────────────
function confirmSignOut() {
confirm.require({
header: 'Sair da conta',
message: 'Tem certeza que deseja sair? Você precisará fazer login novamente.',
icon: 'pi pi-sign-out',
acceptLabel: 'Sair',
rejectLabel: 'Cancelar',
acceptClass: 'p-button-danger',
accept: async () => {
try { await supabase.auth.signOut(); }
finally { router.push('/auth/login'); }
}
});
}
// ── 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); }
}
await tenantStore.ensureLoaded();
await loadProfile();
});
onBeforeUnmount(() => {
if (_mqMobile) {
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
catch { _mqMobile.removeListener(_onMqMobileChange); }
}
clearAvatarFile();
});
</script>
<template>
<!-- Drawer mobile: hospeda a sidebar contextual da pagina (Sua
evolucao + Avatar + Sair) via Teleport target. A navegacao
entre configs vive no botao "Configuracoes" no topo da
.mpr-side, que abre o MelissaConfigPopover. -->
<Transition name="mpr-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="mpr-mobile-drawer"
:class="{ 'is-open': drawerOpen }"
>
<div id="mpr-mobile-drawer-target" class="mpr-mobile-drawer__scroll" />
</div>
</Transition>
<Transition name="mpr-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="mpr-mobile-drawer__backdrop"
@click="fecharDrawer"
/>
</Transition>
<section class="mpr-page">
<header class="mpr-page__head">
<button
class="mpr-menu-btn mpr-menu-btn--mobile-only"
v-tooltip.bottom="'Configurações & Evolução'"
@click="toggleDrawer"
>
<i class="pi pi-bars" />
<span>Menu Perfil</span>
</button>
<div class="mpr-page__title">
<i class="pi pi-user mpr-page__title-icon" />
<span>Meu Perfil</span>
<span class="mpr-page__count">{{ profileProgress }}%</span>
</div>
<div class="mpr-page__actions">
<button
class="mpr-act-btn mpr-act-btn--primary"
v-tooltip.bottom="'Salvar alterações'"
:disabled="!dirty || saving"
@click="saveAll"
>
<i :class="saving ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
<span>Salvar</span>
</button>
<button class="mpr-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
<i class="pi pi-times" />
</button>
</div>
</header>
<div class="mpr-subheader">
<i class="pi pi-info-circle mpr-subheader__icon" />
<span class="mpr-subheader__text">
Suas informações pessoais e indicadores de evolução do perfil. Quanto mais
campos preenchidos, mais alto o seu nível.
</span>
</div>
<div class="mpr-body">
<!-- COL 1: Sidebar (gamificação + avatar). Em desktop
fica em col 1 inline. Em mobile e teleportada pro
drawer "Menu Perfil" (abaixo do menu de configs). -->
<Teleport to="#mpr-mobile-drawer-target" :disabled="!isMobile">
<aside class="mpr-side">
<!-- Botao alterna entre cards (default) e lista
de configs. Click vira "Voltar" quando aberto. -->
<button
class="mpr-cfg-btn"
:class="{ 'is-open': cfgOpen }"
@click="toggleCfg"
>
<i :class="cfgOpen ? 'pi pi-arrow-left' : 'pi pi-cog'" />
<span>{{ cfgOpen ? 'Voltar' : 'Configurações' }}</span>
<i v-if="!cfgOpen" class="pi pi-chevron-down mpr-cfg-btn__chev" />
</button>
<!-- Estado: lista de configs -->
<div v-if="cfgOpen" class="mpr-side__scroll mpr-side__scroll--cfg">
<MelissaConfigList @select="fecharCfg" />
</div>
<!-- Estado: cards contextuais (default) -->
<div v-else class="mpr-side__scroll">
<!-- Card: Sua evolução -->
<div class="mpr-w mpr-w--side">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-star" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Sua evolução</div>
<div class="mpr-w__sub">Nível, conquistas e pendências</div>
</div>
</div>
<div class="mpr-w__body">
<div class="mpr-level">
<div
class="mpr-level__icon"
:style="{
backgroundColor: currentLevel.color + '22',
borderColor: currentLevel.color + '55',
color: currentLevel.color
}"
v-tooltip.top="currentLevel.label"
>{{ currentLevel.icon }}</div>
<div class="mpr-level__text">
<div class="mpr-level__name">
{{ currentLevel.label }}
<span
class="mpr-level__num"
:style="{
backgroundColor: currentLevel.color + '20',
color: currentLevel.color
}"
>Nível {{ gameLevels.indexOf(currentLevel) + 1 }}</span>
</div>
<div class="mpr-level__pct" :style="{ color: progressColor }">
{{ profileProgress }}% completo
</div>
</div>
</div>
<div class="mpr-progress">
<div
class="mpr-progress__bar"
:style="{
width: profileProgress + '%',
backgroundColor: currentLevel.color
}"
/>
</div>
<p class="mpr-progress__hint">
<span v-if="nextLevel" :style="{ color: nextLevel.color }">
{{ xpToNext }}% até {{ nextLevel.label }} {{ nextLevel.icon }}
</span>
<span v-else style="color: #10b981">🎉 Perfil 100% completo!</span>
</p>
<!-- Badges -->
<div class="mpr-badges-head">Conquistas</div>
<div class="mpr-badges">
<button
v-for="badge in badges"
:key="badge.key"
type="button"
class="mpr-badge"
:class="badge.earned ? 'is-earned' : 'is-locked'"
v-tooltip.top="badge.earned ? badge.label : 'Bloqueado ' + badge.label"
@click="scrollToSection(badge.key)"
>
<span class="mpr-badge__icon">{{ badge.icon }}</span>
<span class="mpr-badge__label">{{ badge.label }}</span>
</button>
</div>
<!-- Dicas (campos faltando) -->
<div v-if="progressSuggestions.length" class="mpr-tips">
<div class="mpr-tips__head">O que falta</div>
<button
v-for="(tip, i) in progressSuggestions"
:key="i"
type="button"
class="mpr-tip"
@click="scrollToSection(tip.key)"
>
<i :class="tip.icon" />
{{ tip.text }}
</button>
</div>
</div><!-- /.mpr-w__body -->
</div>
<!-- Card: Avatar -->
<div id="mpr-sec-avatar" class="mpr-w mpr-w--side">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-image" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Avatar</div>
<div class="mpr-w__sub">Foto exibida no menu e cabeçalho</div>
</div>
</div>
<div class="mpr-w__body">
<div class="mpr-avatar">
<div class="mpr-avatar__preview">
<img v-if="ui.avatarPreview" :src="ui.avatarPreview" alt="Avatar" />
<span v-else class="mpr-avatar__initials">{{ initials }}</span>
</div>
<div class="mpr-avatar__actions">
<button class="mpr-btn mpr-btn--primary" @click="pickAvatar">
<i class="pi pi-upload" />
<span>{{ ui.avatarPreview ? 'Trocar' : 'Enviar' }}</span>
</button>
<button
v-if="ui.avatarPreview"
class="mpr-btn mpr-btn--danger"
@click="removeAvatar"
>
<i class="pi pi-times" />
<span>Remover</span>
</button>
</div>
<input
ref="fileInput"
type="file"
accept="image/*"
hidden
@change="onAvatarFileSelected"
/>
<div class="mpr-avatar__hint">JPG, PNG ou WebP. Máximo 5 MB.</div>
</div>
</div><!-- /.mpr-w__body -->
</div>
</div>
<div v-if="!cfgOpen" class="mpr-side__footer">
<button class="mpr-btn mpr-btn--danger mpr-btn--full" @click="confirmSignOut">
<i class="pi pi-sign-out" />
<span>Sair da conta</span>
</button>
</div>
</aside>
</Teleport>
<!-- COL 2: Main (form) -->
<div class="mpr-main">
<!-- Skeleton enquanto carrega -->
<template v-if="loading">
<div class="mpr-w" v-for="n in 3" :key="`sk-${n}`">
<div class="mpr-w__body">
<Skeleton width="40%" height="20px" class="mb-3" />
<Skeleton v-for="m in 3" :key="`sk-${n}-${m}`" width="100%" height="40px" class="mb-2" />
</div>
</div>
</template>
<template v-else>
<!-- Identidade -->
<div id="mpr-sec-identidade" class="mpr-w">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-id-card" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Identidade</div>
<div class="mpr-w__sub">Nome, apelido e descrição profissional</div>
</div>
</div>
<div class="mpr-w__body">
<div class="mpr-grid">
<div class="mpr-field mpr-field--full">
<FloatLabel variant="on">
<InputText
id="mpr_name"
v-model="form.full_name"
class="w-full"
autocomplete="name"
:invalid="!!fieldErrors.full_name"
@input="markDirty(); clearErr('full_name')"
/>
<label for="mpr_name">Nome completo *</label>
</FloatLabel>
<small v-if="fieldErrors.full_name" class="mpr-err">{{ fieldErrors.full_name }}</small>
<small v-else class="mpr-hint">Aparece no menu, cabeçalhos e registros.</small>
</div>
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputText
id="mpr_nickname"
v-model="form.nickname"
class="w-full"
autocomplete="nickname"
:invalid="!!fieldErrors.nickname"
@input="markDirty(); clearErr('nickname')"
/>
<label for="mpr_nickname">Como deveríamos te chamar? *</label>
</FloatLabel>
<small v-if="fieldErrors.nickname" class="mpr-err">{{ fieldErrors.nickname }}</small>
<small v-else class="mpr-hint">Apelido ou nome preferido.</small>
</div>
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<Select
id="mpr_work"
v-model="form.work_description"
:options="workDescriptionOptions"
optionLabel="label"
optionValue="value"
class="w-full"
@change="markDirty"
/>
<label for="mpr_work">Como descreve seu trabalho?</label>
</FloatLabel>
<small class="mpr-hint">Exibido no seu perfil público.</small>
</div>
<div v-if="form.work_description === 'outro'" class="mpr-field mpr-field--full">
<FloatLabel variant="on">
<InputText
id="mpr_work_other"
v-model="form.work_description_other"
class="w-full"
@input="markDirty"
/>
<label for="mpr_work_other">Especifique a profissão</label>
</FloatLabel>
</div>
</div>
</div><!-- /.mpr-w__body -->
</div>
<!-- Registro Profissional (CFP #5) -->
<div id="mpr-sec-registro" class="mpr-w">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-id-card" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Registro profissional</div>
<div class="mpr-w__sub">Conselho regional exigido para emissão de recibos, atestados e laudos</div>
</div>
</div>
<div class="mpr-w__body">
<div class="mpr-grid">
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<Select
id="mpr_reg_type"
v-model="form.professional_registration_type"
:options="REGISTRATION_TYPE_OPTIONS"
optionLabel="label"
optionValue="value"
class="w-full"
@change="markDirty"
/>
<label for="mpr_reg_type">Tipo de registro</label>
</FloatLabel>
<small class="mpr-hint">Conselho profissional ao qual você é vinculado.</small>
</div>
<!-- Campo livre quando tipo='outro' -->
<div v-if="form.professional_registration_type === 'outro'" class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputText
id="mpr_reg_type_other"
v-model="form.professional_registration_type_other"
class="w-full"
@input="markDirty"
/>
<label for="mpr_reg_type_other">Nome do conselho/instituição *</label>
</FloatLabel>
<small class="mpr-hint">Ex: APM, ABRAP, etc.</small>
</div>
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputText
id="mpr_reg_number"
v-model="form.professional_registration_number"
class="w-full"
:disabled="!form.professional_registration_type"
@input="markDirty"
/>
<label for="mpr_reg_number">Número do registro</label>
</FloatLabel>
<small class="mpr-hint">Ex: 06/12345</small>
</div>
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<Select
id="mpr_reg_uf"
v-model="form.professional_registration_uf"
:options="UF_OPTIONS"
optionLabel="label"
optionValue="value"
:disabled="!form.professional_registration_type"
:filter="true"
class="w-full"
@change="markDirty"
/>
<label for="mpr_reg_uf">UF</label>
</FloatLabel>
<small class="mpr-hint">Estado do conselho.</small>
</div>
<div v-if="form.professional_registration_type && form.professional_registration_number" class="mpr-field mpr-field--full">
<div class="mpr-preview-box">
<span class="mpr-preview-label">Aparecerá nos documentos como:</span>
<strong class="mpr-preview-value">
{{ form.professional_registration_type === 'outro'
? (form.professional_registration_type_other || 'Conselho não informado')
: form.professional_registration_type }}
{{ form.professional_registration_number }}{{ form.professional_registration_uf ? '/' + form.professional_registration_uf : '' }}
</strong>
</div>
</div>
</div>
</div><!-- /.mpr-w__body -->
</div>
<!-- Contato -->
<div id="mpr-sec-contato" class="mpr-w">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-phone" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Contato</div>
<div class="mpr-w__sub">WhatsApp e e-mail de login</div>
</div>
</div>
<div class="mpr-w__body">
<div class="mpr-grid">
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputMask
id="mpr_phone"
v-model="form.phone"
mask="(99) 99999-9999"
class="w-full"
:invalid="!!fieldErrors.phone"
@input="markDirty(); clearErr('phone')"
/>
<label for="mpr_phone">WhatsApp *</label>
</FloatLabel>
<small v-if="fieldErrors.phone" class="mpr-err">{{ fieldErrors.phone }}</small>
<small v-else class="mpr-hint">Usado pra atendimento e suporte.</small>
</div>
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputText
id="mpr_email"
:value="userEmail"
readonly
placeholder=" "
class="w-full"
/>
<label for="mpr_email">E-mail (login)</label>
</FloatLabel>
<small class="mpr-hint">Pra trocar, em Segurança.</small>
</div>
</div>
</div><!-- /.mpr-w__body -->
</div>
<!-- Bio -->
<div id="mpr-sec-bio" class="mpr-w">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-pencil" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Bio</div>
<div class="mpr-w__sub">Apresentação curta para o seu perfil público</div>
</div>
</div>
<div class="mpr-w__body">
<FloatLabel variant="on">
<Textarea
id="mpr_bio"
v-model="form.bio"
rows="4"
class="w-full"
@input="markDirty"
/>
<label for="mpr_bio">Conte um pouco sobre você</label>
</FloatLabel>
<small class="mpr-hint">Apresentação curta aparece no seu perfil público.</small>
</div><!-- /.mpr-w__body -->
</div>
<!-- Sites e Redes -->
<div id="mpr-sec-redes" class="mpr-w">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-share-alt" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Sites e Redes</div>
<div class="mpr-w__sub">Site, redes sociais e links customizados</div>
</div>
<button class="mpr-w__action" @click="addCustomSocial">
<i class="pi pi-plus" />
<span>Adicionar</span>
</button>
</div>
<div class="mpr-w__body">
<div class="mpr-grid">
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputText id="mpr_site" v-model="form.site_url" class="w-full" @input="markDirty" />
<label for="mpr_site">Site</label>
</FloatLabel>
</div>
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputText id="mpr_ig" v-model="form.social_instagram" class="w-full" @input="markDirty" />
<label for="mpr_ig">Instagram</label>
</FloatLabel>
</div>
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputText id="mpr_yt" v-model="form.social_youtube" class="w-full" @input="markDirty" />
<label for="mpr_yt">YouTube</label>
</FloatLabel>
</div>
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputText id="mpr_fb" v-model="form.social_facebook" class="w-full" @input="markDirty" />
<label for="mpr_fb">Facebook</label>
</FloatLabel>
</div>
<div class="mpr-field mpr-field--half">
<FloatLabel variant="on">
<InputText id="mpr_x" v-model="form.social_x" class="w-full" @input="markDirty" />
<label for="mpr_x">X (Twitter)</label>
</FloatLabel>
</div>
</div>
<!-- Customs -->
<div v-if="customSocials.length" class="mpr-customs">
<div
v-for="(s, idx) in customSocials"
:key="`cs-${idx}`"
class="mpr-custom"
>
<FloatLabel variant="on" class="flex-1">
<InputText
:id="`cs_name_${idx}`"
v-model="s.name"
class="w-full"
@input="markDirty"
/>
<label :for="`cs_name_${idx}`">Nome (ex: TikTok)</label>
</FloatLabel>
<FloatLabel variant="on" class="flex-1">
<InputText
:id="`cs_url_${idx}`"
v-model="s.url"
class="w-full"
@input="markDirty"
/>
<label :for="`cs_url_${idx}`">URL</label>
</FloatLabel>
<button
class="mpr-btn mpr-btn--danger mpr-btn--icon"
v-tooltip.left="'Remover'"
@click="removeCustomSocial(idx)"
>
<i class="pi pi-trash" />
</button>
</div>
</div>
</div><!-- /.mpr-w__body -->
</div>
<!-- Layout (variante de navegação) -->
<div id="mpr-sec-layout" class="mpr-w">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-th-large" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Layout</div>
<div class="mpr-w__sub">Estilo de navegação principal troca exige reload</div>
</div>
</div>
<div class="mpr-w__body">
<div class="mpr-lv-grid">
<button
class="mpr-lv-card"
:class="{ 'mpr-lv-card--current': layoutConfig.variant === 'classic' }"
:disabled="layoutConfig.variant === 'classic'"
@click="switchToVariant('classic')"
>
<div class="mpr-lv-preview mpr-lv-preview--classic">
<div class="mpr-lv-sidebar" />
<div class="mpr-lv-main">
<div class="mpr-lv-bar" />
<div class="mpr-lv-line" />
<div class="mpr-lv-line mpr-lv-line--sm" />
</div>
</div>
<div class="mpr-lv-foot">
<div class="mpr-lv-radio">
<div v-if="layoutConfig.variant === 'classic'" class="mpr-lv-dot" />
</div>
<div>
<div class="mpr-lv-name">Clássico</div>
<div class="mpr-lv-sub">Sidebar lateral com menu completo</div>
</div>
</div>
</button>
<button
class="mpr-lv-card"
:class="{ 'mpr-lv-card--current': layoutConfig.variant === 'rail' }"
:disabled="layoutConfig.variant === 'rail'"
@click="switchToVariant('rail')"
>
<div class="mpr-lv-preview mpr-lv-preview--rail">
<div class="mpr-lv-rail" />
<div class="mpr-lv-panel" />
<div class="mpr-lv-main">
<div class="mpr-lv-bar" />
<div class="mpr-lv-line" />
<div class="mpr-lv-line mpr-lv-line--sm" />
</div>
</div>
<div class="mpr-lv-foot">
<div class="mpr-lv-radio">
<div v-if="layoutConfig.variant === 'rail'" class="mpr-lv-dot" />
</div>
<div>
<div class="mpr-lv-name">Rail</div>
<div class="mpr-lv-sub">Ícones no canto esquerdo + painel expansível. Disponível apenas no desktop.</div>
</div>
</div>
</button>
<button
class="mpr-lv-card"
:class="{ 'mpr-lv-card--current': layoutConfig.variant === 'melissa' }"
:disabled="layoutConfig.variant === 'melissa'"
@click="switchToVariant('melissa')"
>
<div class="mpr-lv-preview mpr-lv-preview--melissa">
<div class="mpr-lv-melissa-bg" />
<div class="mpr-lv-melissa-dock" />
</div>
<div class="mpr-lv-foot">
<div class="mpr-lv-radio">
<div v-if="layoutConfig.variant === 'melissa'" class="mpr-lv-dot" />
</div>
<div>
<div class="mpr-lv-name">Melissa</div>
<div class="mpr-lv-sub">Lockscreen-style com dock central (atual)</div>
</div>
</div>
</button>
</div>
</div><!-- /.mpr-w__body -->
</div>
<!-- Preferências (tema + aparência) -->
<div id="mpr-sec-preferencias" class="mpr-w">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-palette" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Preferências</div>
<div class="mpr-w__sub">Aparência do sistema</div>
</div>
</div>
<div class="mpr-w__body">
<div class="mpr-grid">
<div class="mpr-field mpr-field--full">
<label class="mpr-label">Tema</label>
<div class="mpr-theme-row">
<button
type="button"
class="mpr-lv-card mpr-theme-card"
:class="{ 'mpr-lv-card--current': !isDarkTheme }"
@click="isDarkTheme && toggleDarkMode()"
>
<i class="pi pi-sun mpr-theme-icon" style="color: #f59e0b;" />
<div class="mpr-theme-text">
<div class="mpr-lv-name">Claro</div>
<div class="mpr-lv-sub">Fundo branco</div>
</div>
<div class="mpr-lv-radio">
<div v-if="!isDarkTheme" class="mpr-lv-dot" />
</div>
</button>
<button
type="button"
class="mpr-lv-card mpr-theme-card"
:class="{ 'mpr-lv-card--current': isDarkTheme }"
@click="!isDarkTheme && toggleDarkMode()"
>
<i class="pi pi-moon mpr-theme-icon" style="color: #6366f1;" />
<div class="mpr-theme-text">
<div class="mpr-lv-name">Escuro</div>
<div class="mpr-lv-sub">Menos fadiga visual</div>
</div>
<div class="mpr-lv-radio">
<div v-if="isDarkTheme" class="mpr-lv-dot" />
</div>
</button>
</div>
<small class="mpr-hint">A preferência é salva no seu perfil.</small>
</div>
</div>
</div><!-- /.mpr-w__body -->
</div>
<!-- Segurança -->
<div id="mpr-sec-seguranca" class="mpr-w">
<div class="mpr-w__head">
<div class="mpr-w__icon"><i class="pi pi-shield" /></div>
<div class="mpr-w__title">
<div class="mpr-w__title-text">Segurança</div>
<div class="mpr-w__sub">Senha e proteção da conta</div>
</div>
</div>
<div class="mpr-w__body">
<div class="mpr-grid">
<div class="mpr-field mpr-field--full">
<div class="mpr-info-row">
<div class="mpr-info-text">
<div class="mpr-info-title">E-mail de acesso</div>
<div class="mpr-info-value">{{ userEmail }}</div>
</div>
<small class="mpr-hint mpr-info-hint">Para trocar o e-mail, contate o suporte.</small>
</div>
</div>
<div class="mpr-field mpr-field--full">
<button type="button" class="mpr-lv-card mpr-action-card" @click="goSeguranca">
<i class="pi pi-key mpr-action-icon" />
<div class="mpr-action-text">
<div class="mpr-lv-name">Trocar senha</div>
<div class="mpr-lv-sub">Atualize sua senha de acesso ao sistema</div>
</div>
<i class="pi pi-arrow-right mpr-action-arrow" />
</button>
</div>
</div>
</div><!-- /.mpr-w__body -->
</div>
</template>
</div>
</div>
</section>
</template>
<style scoped>
/* ═══════ Page chrome ═══════ */
.mpr-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: mpr-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
@keyframes mpr-page-enter {
from { opacity: 0; transform: scale(0.985); }
to { opacity: 1; transform: scale(1); }
}
.mpr-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;
}
.mpr-page__title {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
font-weight: 700;
color: var(--m-text);
}
.mpr-page__title-icon {
color: var(--p-primary-color);
font-size: 1.05rem;
}
.mpr-page__count {
margin-left: auto;
background: color-mix(in srgb, var(--p-primary-color) 18%, transparent);
color: var(--p-primary-color);
border-radius: 999px;
padding: 3px 10px;
font-size: 0.74rem;
font-weight: 700;
}
.mpr-page__actions {
display: flex;
align-items: center;
gap: 6px;
}
.mpr-act-btn {
display: inline-flex;
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;
transition: background-color 120ms ease, border-color 120ms ease, opacity 120ms ease;
}
.mpr-act-btn:hover {
background: var(--m-bg-soft-hover);
}
.mpr-act-btn--primary {
background: var(--p-primary-color);
border-color: var(--p-primary-color);
color: var(--p-primary-contrast-color, #fff);
}
.mpr-act-btn--primary:hover {
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
}
.mpr-act-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.mpr-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;
}
.mpr-close:hover {
background: var(--m-bg-soft-hover);
color: var(--m-text);
}
.mpr-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;
}
.mpr-menu-btn:hover { background: var(--m-bg-soft-hover); }
.mpr-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;
}
.mpr-subheader__icon {
color: var(--p-primary-color);
font-size: 0.85rem;
margin-top: 2px;
flex-shrink: 0;
}
/* ═══════ Body 2-col ═══════ */
.mpr-body {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
/* Botao "Configuracoes" no topo da .mpr-side. Click alterna entre
cards (default) e lista de configs (estado is-open: vira "Voltar"). */
.mpr-cfg-btn {
display: flex;
align-items: center;
gap: 8px;
width: calc(100% - 24px);
margin: 12px 12px 0;
padding: 10px 12px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
border-radius: 9px;
color: var(--m-text);
cursor: pointer;
font-family: inherit;
font-size: 0.84rem;
font-weight: 600;
text-align: left;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
flex-shrink: 0;
}
.mpr-cfg-btn:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mpr-cfg-btn.is-open {
background: color-mix(in srgb, var(--p-primary-color) 14%, transparent);
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
color: var(--p-primary-color);
}
.mpr-cfg-btn > i:first-child {
color: var(--p-primary-color);
font-size: 0.92rem;
}
.mpr-cfg-btn > span {
flex: 1;
}
.mpr-cfg-btn__chev {
color: var(--m-text-muted);
font-size: 0.7rem;
}
/* Variante do scroll quando renderiza a lista de configs em vez
dos cards — sem gap entre items (a propria lista controla). */
.mpr-side__scroll--cfg {
padding: 8px;
gap: 0;
}
/* COL 1: sidebar */
.mpr-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;
}
.mpr-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;
}
.mpr-side__scroll::-webkit-scrollbar { width: 5px; }
.mpr-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mpr-side__footer {
flex-shrink: 0;
padding: 12px;
background: var(--m-bg-medium);
border-top: 1px solid var(--m-border);
}
/* COL 2: main */
.mpr-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;
}
.mpr-main::-webkit-scrollbar { width: 5px; }
.mpr-main::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
/* Desktop (>=1024px): cards em 2 colunas (50/50) no main. Internal
grid colapsa pra 1 col pra nao ficar cramped. Cada card tem
min-height 300px + max-height 100% (do container) e overflow-y:
auto no body — adicionar redes/customs nao quebra layout, scroll
interno engata. Mesma logica nos cards da sidebar (Sua evolucao,
Avatar) que cresciam quando muitas badges/dicas apareciam. */
@media (min-width: 1024px) {
/* Fake dialog: largura adaptativa, alinhada a esquerda (apos o
config-aside global). Espelha pattern de Seguranca/Pagamento/etc:
- 10241012px : full-width (right: 6px) — overlap minimo
- 10122012px : width = 1000px fixo (right cresce com viewport)
- >= 2012px : width = ~50% do viewport (right: 50%) */
.mpr-page {
right: max(6px, min(50%, calc(100% - 1006px)));
}
/* Cards do main empilhados em 1-col (era grid 2-col antes).
.mpr-main herda flex-direction: column do base — cards crescem
com conteudo, sem scroll interno. Cada card ocupa width total
do dialog (max ~1000px). */
.mpr-main > .mpr-w {
flex-shrink: 0;
height: auto;
}
/* Cards da sidebar (Sua evolucao + Avatar): altura natural,
flex-shrink: 0 evita compressao quando total passa do
.mpr-side__scroll (scroll externo). */
.mpr-side > .mpr-side__scroll > .mpr-w--side {
display: flex;
flex-direction: column;
flex-shrink: 0;
height: auto;
}
}
/* ═══════ Card-base (mpr-w) — visual igual aos cards do MelissaFinanceiro:
head com icon-box colorido + titulo + subtitulo + border-bottom; body
com padding interno separado; box-shadow pra elevacao. */
.mpr-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;
}
/* Cards na sidebar — bg --m-bg-medium destacando do bg --m-bg-soft
da .mpr-side (espelha o pattern do MelissaPacientes .mp-w). */
.mpr-w--side {
background: var(--m-bg-medium);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
}
.mpr-w__head {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid var(--m-border);
}
.mpr-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);
font-size: 1rem;
}
.mpr-w__icon > i { font-size: 0.95rem; }
.mpr-w__title {
flex: 1;
min-width: 0;
}
.mpr-w__title-text {
font-size: 0.92rem;
font-weight: 700;
color: var(--m-text);
line-height: 1.2;
}
.mpr-w__sub {
font-size: 0.74rem;
color: var(--m-text-muted);
margin-top: 2px;
line-height: 1.3;
}
.mpr-w__action {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 10px;
border-radius: 7px;
border: 1px solid var(--m-border);
background: var(--m-bg-medium);
color: var(--m-text);
cursor: pointer;
font-size: 0.74rem;
font-weight: 600;
flex-shrink: 0;
transition: background-color 120ms ease;
}
.mpr-w__action:hover { background: var(--m-bg-soft-hover); }
.mpr-w__action > i { font-size: 0.72rem; color: var(--p-primary-color); }
/* Body do card — wrapper unificado pra padding/gap consistente */
.mpr-w__body {
padding: 14px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* ═══════ Gamificação ═══════ */
.mpr-level {
display: flex;
align-items: center;
gap: 10px;
}
.mpr-level__icon {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: 12px;
border: 1px solid;
font-size: 1.5rem;
flex-shrink: 0;
}
.mpr-level__text {
flex: 1;
min-width: 0;
}
.mpr-level__name {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.92rem;
font-weight: 700;
color: var(--m-text);
}
.mpr-level__num {
font-size: 0.66rem;
font-weight: 700;
padding: 2px 7px;
border-radius: 999px;
}
.mpr-level__pct {
font-size: 0.74rem;
font-weight: 700;
margin-top: 2px;
}
.mpr-progress {
width: 100%;
height: 8px;
border-radius: 999px;
background: var(--m-bg-soft);
overflow: hidden;
}
.mpr-progress__bar {
height: 100%;
border-radius: 999px;
transition: width 600ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.mpr-progress__hint {
margin: 0;
font-size: 0.72rem;
color: var(--m-text-muted);
font-weight: 600;
}
.mpr-badges-head {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--m-text-muted);
}
.mpr-badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mpr-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid var(--m-border);
font-size: 0.7rem;
font-family: inherit;
cursor: pointer;
transition: transform 120ms ease, background-color 120ms ease;
}
.mpr-badge:hover { transform: translateY(-1px); }
.mpr-badge.is-earned {
background: color-mix(in srgb, var(--p-primary-color) 14%, transparent);
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
color: var(--p-primary-color);
font-weight: 600;
}
.mpr-badge.is-locked {
background: var(--m-bg-soft);
color: var(--m-text-faint);
opacity: 0.6;
}
.mpr-badge__icon { font-size: 0.85rem; line-height: 1; }
.mpr-badge__label { line-height: 1; }
.mpr-tips {
display: flex;
flex-direction: column;
gap: 5px;
padding-top: 8px;
border-top: 1px solid var(--m-border);
}
.mpr-tips__head {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--m-text-muted);
}
.mpr-tip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 6px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text-muted);
font-size: 0.72rem;
font-family: inherit;
cursor: pointer;
text-align: left;
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.mpr-tip:hover {
background: color-mix(in srgb, var(--p-primary-color) 12%, transparent);
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
color: var(--p-primary-color);
}
.mpr-tip > i { font-size: 0.7rem; color: var(--p-primary-color); }
/* ═══════ Avatar (sidebar) ═══════ */
.mpr-avatar {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 4px 0 0;
}
.mpr-avatar__preview {
width: 96px;
height: 96px;
border-radius: 18px;
border: 1px solid var(--m-border);
background: var(--m-bg-soft);
overflow: hidden;
display: grid;
place-items: center;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}
.mpr-avatar__preview > img {
width: 100%;
height: 100%;
object-fit: cover;
}
.mpr-avatar__initials {
font-size: 2rem;
font-weight: 800;
color: var(--m-text);
}
.mpr-avatar__actions {
display: flex;
gap: 6px;
width: 100%;
justify-content: center;
}
.mpr-avatar__hint {
font-size: 0.7rem;
color: var(--m-text-muted);
text-align: center;
}
/* ═══════ Buttons (sidebar utilitarios) ═══════ */
.mpr-btn {
display: inline-flex;
align-items: center;
justify-content: 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.78rem;
font-weight: 600;
transition: background-color 120ms ease;
}
.mpr-btn:hover { background: var(--m-bg-soft-hover); }
.mpr-btn--primary {
background: var(--p-primary-color);
border-color: var(--p-primary-color);
color: var(--p-primary-contrast-color, #fff);
}
.mpr-btn--primary:hover {
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
}
.mpr-btn--danger {
background: rgba(220, 38, 38, 0.12);
border-color: rgba(220, 38, 38, 0.40);
color: rgb(220, 38, 38);
}
.mpr-btn--danger:hover {
background: rgba(220, 38, 38, 0.22);
}
.mpr-btn--full { width: 100%; }
.mpr-btn--icon {
padding: 7px 10px;
flex-shrink: 0;
}
/* ═══════ Form grid (main) ═══════ */
.mpr-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px 12px;
margin-top: 4px;
}
.mpr-field { min-width: 0; }
.mpr-field--full { grid-column: 1 / -1; }
.mpr-field--half { grid-column: span 1; }
.mpr-hint, .mpr-err {
display: block;
margin-top: 4px;
font-size: 0.7rem;
line-height: 1.3;
}
.mpr-hint { color: var(--m-text-muted); }
.mpr-err { color: rgb(220, 38, 38); font-weight: 500; }
/* ═══════ Custom socials ═══════ */
.mpr-customs {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
padding-top: 10px;
border-top: 1px dashed var(--m-border);
}
.mpr-custom {
display: flex;
gap: 8px;
align-items: stretch;
}
.mpr-custom > .p-floatlabel,
.mpr-custom > div { min-width: 0; }
/* ═══════ Mobile drawer — agora hospeda o MENU GLOBAL DE CONFIGS
no topo + a sidebar contextual (Sua evolucao + Avatar) abaixo via
Teleport target. Em desktop o drawer fica oculto e o
MelissaLayout renderiza o menu fixado na esquerda. */
.mpr-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;
}
.mpr-mobile-drawer.is-open { transform: translateX(0); }
.mpr-mobile-drawer__scroll {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Aside teleportada pro drawer — perde adornos proprios */
.mpr-mobile-drawer__scroll .mpr-side {
flex: 1;
min-height: 0;
width: 100%;
overflow: hidden;
background: transparent;
border-right: none;
display: flex;
flex-direction: column;
}
.mpr-mobile-drawer__scroll .mpr-cfg-btn {
margin: 12px 12px 0;
flex-shrink: 0;
}
.mpr-mobile-drawer__scroll .mpr-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;
}
.mpr-mobile-drawer__scroll .mpr-side__scroll::-webkit-scrollbar { width: 5px; }
.mpr-mobile-drawer__scroll .mpr-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mpr-mobile-drawer__scroll .mpr-w--side {
margin: 0;
flex-shrink: 0;
}
.mpr-mobile-drawer__scroll .mpr-side__footer {
flex-shrink: 0;
margin: 0;
padding: 12px;
background: var(--m-bg-medium);
border-top: 1px solid var(--m-border);
}
.mpr-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;
}
.mpr-drawer-fade-enter-active,
.mpr-drawer-fade-leave-active { transition: opacity 200ms ease; }
.mpr-drawer-fade-enter-from,
.mpr-drawer-fade-leave-to { opacity: 0; }
/* ═══════ Mobile (<1024px) ═══════ */
@media (max-width: 1023px) {
.mpr-body {
flex-direction: column;
padding: 0;
}
/* .mpr-side foi Teleportada pro drawer — nao precisa de display: none
(Teleport ja a removeu do .mpr-body). Aplicar display: none aqui
escondia o elemento teleportado dentro do drawer. */
.mpr-main {
width: 100%;
padding: 8px;
}
/* Cards em mobile: altura natural por conteudo (sem stretch
vertical do flex column do .mpr-main, sem clip por overflow). */
.mpr-main .mpr-w {
height: auto;
flex: 0 0 auto;
align-self: stretch;
}
.mpr-grid {
grid-template-columns: 1fr;
}
.mpr-field--half { grid-column: 1 / -1; }
.mpr-page__title > span:first-of-type { display: none; }
.mpr-page__title-icon { display: none; }
.mpr-menu-btn--mobile-only { display: inline-flex; }
/* Botao Salvar mostra o texto em mobile (acao primaria importante) */
.mpr-custom { flex-direction: column; gap: 8px; }
.mpr-custom .mpr-btn--icon { align-self: flex-end; }
}
/* ═══════ Layout variant cards ═══════ */
.mpr-lv-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 14px;
}
.mpr-lv-card {
background: var(--m-surface-2, var(--surface-card));
border: 1px solid var(--m-border, var(--surface-border));
border-radius: 10px;
padding: 12px;
text-align: left;
cursor: pointer;
transition: border-color .15s, background .15s, transform .12s;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--text-color);
}
.mpr-lv-card:hover:not(:disabled) {
border-color: var(--p-primary-500, #7c3aed);
background: var(--m-surface-hover, var(--surface-hover));
}
.mpr-lv-card:disabled {
cursor: default;
opacity: 0.9;
}
.mpr-lv-card--current {
border-color: var(--p-primary-500, #7c3aed);
box-shadow: 0 0 0 1px var(--p-primary-500, #7c3aed) inset;
}
.mpr-lv-preview {
height: 92px;
border-radius: 6px;
background: var(--m-surface-3, var(--surface-100));
border: 1px solid var(--m-border, var(--surface-border));
position: relative;
overflow: hidden;
display: flex;
}
.mpr-lv-preview--classic .mpr-lv-sidebar {
width: 30%;
background: var(--p-primary-500, #7c3aed);
opacity: 0.85;
}
.mpr-lv-preview--rail .mpr-lv-rail {
width: 12%;
background: var(--p-primary-500, #7c3aed);
opacity: 0.85;
}
.mpr-lv-preview--rail .mpr-lv-panel {
width: 22%;
background: var(--p-primary-500, #7c3aed);
opacity: 0.35;
}
.mpr-lv-main {
flex: 1;
padding: 8px;
display: flex;
flex-direction: column;
gap: 5px;
}
.mpr-lv-bar {
height: 8px;
background: var(--m-border-strong, var(--surface-300));
border-radius: 2px;
opacity: 0.6;
}
.mpr-lv-line {
height: 5px;
background: var(--m-border-strong, var(--surface-300));
border-radius: 2px;
opacity: 0.4;
}
.mpr-lv-line--sm { width: 65%; }
.mpr-lv-preview--melissa {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
}
.mpr-lv-melissa-bg {
position: absolute;
inset: 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.08);
}
.mpr-lv-melissa-dock {
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 14px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.25);
}
.mpr-lv-foot {
display: flex;
align-items: flex-start;
gap: 9px;
}
.mpr-lv-radio {
width: 14px;
height: 14px;
border-radius: 9999px;
border: 1.5px solid var(--m-border-strong, var(--surface-400));
display: grid;
place-items: center;
flex-shrink: 0;
margin-top: 3px;
}
.mpr-lv-card--current .mpr-lv-radio {
border-color: var(--p-primary-500, #7c3aed);
}
.mpr-lv-dot {
width: 7px;
height: 7px;
border-radius: 9999px;
background: var(--p-primary-500, #7c3aed);
}
.mpr-lv-name {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-color);
line-height: 1.2;
}
.mpr-lv-sub {
font-size: 0.75rem;
color: var(--text-color-secondary);
line-height: 1.3;
margin-top: 2px;
}
/* ═══════════ Registro Profissional — preview box ═══════════ */
.mpr-preview-box {
background: color-mix(in srgb, var(--p-primary-color) 7%, transparent);
border: 1px solid color-mix(in srgb, var(--p-primary-color) 22%, var(--surface-border));
border-radius: 8px;
padding: 10px 14px;
font-size: 0.88rem;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.mpr-preview-label {
color: var(--text-color-secondary);
}
.mpr-preview-value {
color: var(--text-color);
font-weight: 600;
letter-spacing: 0.01em;
}
/* ═══════════ Preferências — tema em 1 linha ═══════════ */
.mpr-theme-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.mpr-theme-card {
padding: 14px 16px;
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
text-align: left;
}
.mpr-theme-icon {
font-size: 1.3rem;
flex-shrink: 0;
}
.mpr-theme-text {
flex: 1;
min-width: 0;
}
/* ═══════════ Segurança — info row + action card ═══════════ */
.mpr-info-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 14px;
border: 1px solid var(--surface-border);
border-radius: 8px;
background: var(--surface-ground, transparent);
}
.mpr-info-text {
flex: 1;
min-width: 0;
}
.mpr-info-title {
font-weight: 600;
margin-bottom: 4px;
color: var(--text-color);
}
.mpr-info-value {
color: var(--text-color-secondary);
font-size: 0.9rem;
}
.mpr-info-hint {
margin: 0;
flex-shrink: 0;
}
.mpr-action-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 18px;
width: 100%;
text-align: left;
cursor: pointer;
}
.mpr-action-icon {
font-size: 1.4rem;
color: var(--text-color);
flex-shrink: 0;
}
.mpr-action-text {
flex: 1;
min-width: 0;
}
.mpr-action-arrow {
opacity: 0.5;
flex-shrink: 0;
}
</style>