MelissaPerfil mobile: menu de configs inline no topo (sem drawer)

Refator do menu mobile so no Perfil (validacao do padrao):
- Remove o drawer slide-in da esquerda + backdrop + botao "Menu"/
  "Configuracoes" no header
- Renderiza <MelissaConfigSidebar> INLINE no topo do .mpr-body em
  mobile (.mpr-mobile-config-menu)
- A sidebar contextual (Sua evolucao + Avatar) tambem renderiza
  inline em sequencia, abaixo do menu global
- O main com o form fica abaixo de tudo
- Body em mobile vira flex column + overflow-y: auto (scroll externo
  unico pra toda a pagina)

Drawer state (drawerOpen/toggle/fechar) e Teleport removidos do
JS+template. Em desktop nada muda — MelissaLayout segue renderizando
a sidebar global fixa na esquerda.

Pendente: aplicar o mesmo pattern nas outras 8 paginas de config se
o usuario validar este formato.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-06 18:01:34 -03:00
parent 989c5330f8
commit d1dced242f
+59 -117
View File
@@ -21,6 +21,7 @@ import { useConfirm } from 'primevue/useconfirm';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { supabase } from '@/lib/supabase/client'; import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore'; import { useTenantStore } from '@/stores/tenantStore';
import MelissaConfigSidebar from './MelissaConfigSidebar.vue';
// InputText/Select/Textarea/InputMask/Skeleton/Tag/Button: auto via PrimeVueResolver // InputText/Select/Textarea/InputMask/Skeleton/Tag/Button: auto via PrimeVueResolver
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
@@ -32,16 +33,11 @@ const tenantStore = useTenantStore();
const AVATAR_BUCKET = 'avatars'; const AVATAR_BUCKET = 'avatars';
// ── Breakpoints + drawer ─────────────────────────────────── // ── Breakpoints (sem drawer — em mobile o menu de configs renderiza
const drawerOpen = ref(false); // inline no topo via MelissaConfigSidebar). ─────────────────
const isMobile = ref(false); const isMobile = ref(false);
let _mqMobile = null; let _mqMobile = null;
function _onMqMobileChange(e) { function _onMqMobileChange(e) { isMobile.value = e.matches; }
isMobile.value = e.matches;
if (!e.matches) drawerOpen.value = false;
}
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
function fecharDrawer() { drawerOpen.value = false; }
// ── Estado ───────────────────────────────────────────────── // ── Estado ─────────────────────────────────────────────────
const loading = ref(true); const loading = ref(true);
@@ -420,10 +416,6 @@ const SECTION_BY_FIELD = {
function scrollToSection(field) { function scrollToSection(field) {
const sec = SECTION_BY_FIELD[field]; const sec = SECTION_BY_FIELD[field];
if (!sec) return; if (!sec) return;
// Avatar fica na sidebar; em mobile mantem o drawer aberto (e nao scrolla
// o main, pq a sessao nao existe no main).
const isAvatar = sec === 'avatar';
if (isMobile.value && !isAvatar) drawerOpen.value = false;
nextTick(() => { nextTick(() => {
const target = document.getElementById('mpr-sec-' + sec); const target = document.getElementById('mpr-sec-' + sec);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -468,34 +460,8 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<!-- Backdrop + drawer mobile pra sidebar (Teleport target) -->
<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"> <section class="mpr-page">
<header class="mpr-page__head"> <header class="mpr-page__head">
<button
class="mpr-menu-btn mpr-menu-btn--mobile-only"
v-tooltip.bottom="'Evolução & Avatar'"
@click="toggleDrawer"
>
<i class="pi pi-bars" />
<span>Menu Perfil</span>
</button>
<div class="mpr-page__title"> <div class="mpr-page__title">
<i class="pi pi-user mpr-page__title-icon" /> <i class="pi pi-user mpr-page__title-icon" />
<span>Meu Perfil</span> <span>Meu Perfil</span>
@@ -526,8 +492,16 @@ onBeforeUnmount(() => {
</div> </div>
<div class="mpr-body"> <div class="mpr-body">
<!-- COL 1: Sidebar (gamificação + avatar) --> <!-- Menu global de configs SO em mobile, no topo do body
<Teleport to="#mpr-mobile-drawer-target" :disabled="!isMobile"> (em desktop o MelissaLayout ja renderiza a sidebar
fixada na esquerda fora do .mpr-page). -->
<div v-if="isMobile" class="mpr-mobile-config-menu">
<MelissaConfigSidebar />
</div>
<!-- COL 1: Sidebar (gamificação + avatar) inline em
desktop e mobile. Em mobile o .mpr-body e flex column,
entao essa aside aparece acima do main. -->
<aside class="mpr-side"> <aside class="mpr-side">
<div class="mpr-side__scroll"> <div class="mpr-side__scroll">
<!-- Card: Sua evolução --> <!-- Card: Sua evolução -->
@@ -668,7 +642,6 @@ onBeforeUnmount(() => {
</button> </button>
</div> </div>
</aside> </aside>
</Teleport>
<!-- COL 2: Main (form) --> <!-- COL 2: Main (form) -->
<div class="mpr-main"> <div class="mpr-main">
@@ -1498,97 +1471,66 @@ onBeforeUnmount(() => {
.mpr-custom > .p-floatlabel, .mpr-custom > .p-floatlabel,
.mpr-custom > div { min-width: 0; } .mpr-custom > div { min-width: 0; }
/* ═══════ Mobile drawer (sidebar teleportada) ═══════ */ /* ═══════ Menu global de configs inline em mobile (topo do body)
.mpr-mobile-drawer { — substitui o drawer antigo. Em desktop fica oculto pq o
position: fixed; MelissaLayout ja renderiza a sidebar global fixada na esquerda. */
top: 0; left: 0; .mpr-mobile-config-menu {
height: 100vh; display: none;
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;
}
.mpr-mobile-drawer__scroll .mpr-side {
flex: 1;
min-height: 0;
width: 100%; width: 100%;
overflow: hidden;
background: transparent;
border-right: none;
display: flex;
flex-direction: column;
}
.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; 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) ═══════ */ /* ═══════ Mobile (<1024px) ═══════ */
@media (max-width: 1023px) { @media (max-width: 1023px) {
/* Body vira flex column + scroll vertical externo. Tudo (menu
global, sidebar contextual, main) stack verticalmente. */
.mpr-body { .mpr-body {
flex-direction: column; flex-direction: column;
padding: 0; padding: 0;
overflow-y: auto;
overflow-x: hidden;
} }
/* Menu global de configs no topo */
.mpr-mobile-config-menu {
display: block;
padding: 8px 8px 0;
flex: 0 0 auto;
}
.mpr-mobile-config-menu :deep(.mcs-aside) {
height: auto;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.mpr-mobile-config-menu :deep(.mcs-aside__body) {
max-height: none;
overflow: visible;
}
/* Sidebar contextual (Sua evolucao + Avatar) inline */
.mpr-side { .mpr-side {
display: none; width: 100%;
flex: 0 0 auto;
background: transparent;
border-right: none;
padding: 8px 8px 0;
}
.mpr-side__scroll {
padding: 0;
gap: 8px;
overflow: visible;
}
.mpr-side__footer {
padding: 8px 0 0;
background: transparent;
border-top: none;
}
.mpr-w--side {
flex-shrink: 0;
} }
.mpr-main { .mpr-main {
width: 100%; width: 100%;
padding: 8px; padding: 8px;
overflow: visible;
flex: 0 0 auto;
} }
/* Cards em mobile: altura natural por conteudo (sem stretch /* Cards em mobile: altura natural por conteudo (sem stretch
vertical do flex column do .mpr-main, sem clip por overflow). */ vertical do flex column do .mpr-main, sem clip por overflow). */