templates/editor: drawer mobile com form + variaveis (tabs)

Mobile (<1024px): so o editor (col 2) fica visivel. Form de
metadados (col 1) e variaveis (col 3) viram tabs dentro de um
drawer fixed que abre pela esquerda.

Padrao espelhado de MelissaBloqueios/MelissaDocumentosTemplates,
com adaptacoes pra ser autocontido (sem dependencia do componente
pai).

Script:
- drawerOpen + drawerTab ('form' | 'vars') + isMobile refs
- _mqMobile matchMedia listener (onMounted setup +
  onBeforeUnmount cleanup)
- openDrawer(tab) / fecharDrawer helpers
- insertVariable agora fecha o drawer no mobile apos inserir

Template:
- Drawer wrap no inicio: tabs (Identificacao / Variaveis) +
  botao close + 2 panes (#dte-mobile-drawer-form e
  #dte-mobile-drawer-vars)
- Backdrop overlay com blur fecha o drawer
- Toolbar do editor ganha 2 botoes mobile-only (Identificacao /
  Variaveis) com classe dte-toolbar__mobile-actions
- <Teleport to="#dte-mobile-drawer-form" :disabled="!isMobile">
  envolvendo a <aside class="dte-side">
- <Teleport to="#dte-mobile-drawer-vars" :disabled="!isMobile">
  envolvendo a <aside class="dte-vars">

CSS:
- .dte-mobile-drawer fixed left, transform translateX, 250ms
- 2 panes scroll interno separado
- @media (max-width:1023px): cols vira 1-col, side/vars inline
  somem, botoes mobile aparecem, titulo canonico some

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-21 17:23:13 -03:00
parent bbbb08ba9d
commit 134f562a1f
@@ -9,7 +9,7 @@
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
--> -->
<script setup> <script setup>
import { ref, watch, computed } from 'vue' import { ref, watch, computed, onMounted, onBeforeUnmount } from 'vue'
import { useDocumentTemplates } from '../composables/useDocumentTemplates' import { useDocumentTemplates } from '../composables/useDocumentTemplates'
import JoditEmailEditor from '@/components/ui/JoditEmailEditor.vue' import JoditEmailEditor from '@/components/ui/JoditEmailEditor.vue'
@@ -82,6 +82,9 @@ function insertVariable(varKey) {
if (!form.value.variaveis.includes(varKey)) { if (!form.value.variaveis.includes(varKey)) {
form.value.variaveis = [...form.value.variaveis, varKey] form.value.variaveis = [...form.value.variaveis, varKey]
} }
// No mobile, fecha o drawer pra liberar a tela do editor
if (isMobile.value) drawerOpen.value = false;
} }
// ── Save ──────────────────────────────────────────────────── // ── Save ────────────────────────────────────────────────────
@@ -89,6 +92,40 @@ function insertVariable(varKey) {
function onSave() { function onSave() {
emit('save', { ...form.value }) emit('save', { ...form.value })
} }
// ── Mobile drawer (espelha padrão MelissaBloqueios/Templates) ─
// No mobile, form (col 1) + variáveis (col 3) viram tabs dentro
// de um drawer único. Só o editor (col 2) fica visível na tela.
const drawerOpen = ref(false);
const drawerTab = ref('form'); // form | vars
const isMobile = ref(false);
let _mqMobile = null;
function _onMqMobileChange(e) {
isMobile.value = e.matches;
if (!e.matches) drawerOpen.value = false;
}
function openDrawer(tab) {
drawerTab.value = tab || 'form';
drawerOpen.value = true;
}
function fecharDrawer() { drawerOpen.value = false; }
onMounted(() => {
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); }
}
});
onBeforeUnmount(() => {
if (_mqMobile) {
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
catch { _mqMobile.removeListener(_onMqMobileChange); }
}
});
</script> </script>
<style scoped> <style scoped>
@@ -417,21 +454,148 @@ function onSave() {
color: #666; color: #666;
} }
/* ═══════ Mobile (<1024px): empilha 1 col ═══════ */ /* ═══════ Toolbar mobile actions (botões "Identificação" / "Variáveis") ═══════ */
.dte-toolbar__mobile-actions {
display: none;
align-items: center;
gap: 6px;
}
.dte-mobile-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
border-radius: 8px;
border: 1px solid var(--surface-border);
background: var(--surface-ground);
color: var(--text-color);
cursor: pointer;
font-size: 0.78rem;
font-weight: 600;
flex-shrink: 0;
font-family: inherit;
transition: background-color 120ms ease;
}
.dte-mobile-btn:hover { background: color-mix(in srgb, var(--p-primary-color) 8%, transparent); }
.dte-mobile-btn > i { color: var(--p-primary-color); font-size: 0.82rem; }
/* ═══════ Mobile drawer (form + variáveis em tabs) ═══════ */
.dte-mobile-drawer {
position: fixed;
top: 0; left: 0;
height: 100vh;
height: 100dvh;
width: min(360px, 92vw);
z-index: 80;
background: var(--surface-card);
border-right: 1px solid var(--surface-border);
transform: translateX(-100%);
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--text-color);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
display: flex;
flex-direction: column;
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.18);
}
.dte-mobile-drawer.is-open { transform: translateX(0); }
.dte-mobile-drawer__tabs {
display: flex;
align-items: stretch;
gap: 0;
border-bottom: 1px solid var(--surface-border);
background: var(--surface-ground);
flex-shrink: 0;
}
.dte-drawer-tab {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px 14px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-color-secondary);
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
font-family: inherit;
transition: color 140ms ease, border-color 140ms ease, background-color 140ms ease;
}
.dte-drawer-tab:hover {
color: var(--text-color);
background: color-mix(in srgb, var(--p-primary-color) 4%, transparent);
}
.dte-drawer-tab.is-active {
color: var(--p-primary-color);
border-bottom-color: var(--p-primary-color);
}
.dte-drawer-tab > i { font-size: 0.82rem; }
.dte-drawer-close {
width: 44px;
display: grid;
place-items: center;
background: transparent;
border: none;
color: var(--text-color-secondary);
cursor: pointer;
border-left: 1px solid var(--surface-border);
transition: background-color 120ms ease, color 120ms ease;
}
.dte-drawer-close:hover {
background: var(--surface-ground);
color: var(--text-color);
}
.dte-mobile-drawer__pane {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
scrollbar-width: thin;
}
.dte-mobile-drawer__pane > .dte-side,
.dte-mobile-drawer__pane > .dte-vars {
border: none;
border-radius: 0;
flex: 1;
width: 100%;
}
.dte-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;
}
.dte-drawer-fade-enter-active,
.dte-drawer-fade-leave-active { transition: opacity 200ms ease; }
.dte-drawer-fade-enter-from,
.dte-drawer-fade-leave-to { opacity: 0; }
/* ═══════ Mobile (<1024px): só o editor visível ═══════ */
@media (max-width: 1023px) { @media (max-width: 1023px) {
/* Editor ocupa tela inteira — col 1 e col 3 viram drawer */
.dte-cols { .dte-cols {
grid-template-columns: 1fr; grid-template-columns: 1fr;
overflow-y: auto; overflow: hidden;
align-items: start;
}
.dte-side,
.dte-main,
.dte-vars {
height: auto;
}
.dte-vars__list {
max-height: 320px;
} }
.dte-cols > .dte-side,
.dte-cols > .dte-vars { display: none; }
/* Mostra os botões "Identificação" / "Variáveis" no header */
.dte-toolbar__mobile-actions { display: inline-flex; }
/* Esconde o título canônico no mobile (espaço pros botões) */
.dte-toolbar__title > span { display: none; }
.dte-toolbar__title > i { display: none; }
.dte-preview { .dte-preview {
padding: 12px; padding: 12px;
} }
@@ -442,9 +606,77 @@ function onSave() {
</style> </style>
<template> <template>
<!-- Mobile drawer (form + variáveis em tabs) -->
<Transition name="dte-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="dte-mobile-drawer"
:class="{ 'is-open': drawerOpen }"
>
<div class="dte-mobile-drawer__tabs">
<button
type="button"
class="dte-drawer-tab"
:class="{ 'is-active': drawerTab === 'form' }"
@click="drawerTab = 'form'"
>
<i class="pi pi-tag" />
<span>Identificação</span>
</button>
<button
type="button"
class="dte-drawer-tab"
:class="{ 'is-active': drawerTab === 'vars' }"
@click="drawerTab = 'vars'"
>
<i class="pi pi-code" />
<span>Variáveis</span>
</button>
<button
type="button"
class="dte-drawer-close"
v-tooltip.bottom="'Fechar'"
@click="fecharDrawer"
>
<i class="pi pi-times" />
</button>
</div>
<div id="dte-mobile-drawer-form" v-show="drawerTab === 'form'" class="dte-mobile-drawer__pane" />
<div id="dte-mobile-drawer-vars" v-show="drawerTab === 'vars'" class="dte-mobile-drawer__pane" />
</div>
</Transition>
<Transition name="dte-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="dte-mobile-drawer__backdrop"
@click="fecharDrawer"
/>
</Transition>
<div class="dte-page"> <div class="dte-page">
<!-- Toggle Editor / Preview no topo --> <!-- Toggle Editor / Preview no topo -->
<div class="dte-toolbar"> <div class="dte-toolbar">
<!-- Botões "Identificação" e "Variáveis" mobile-only -->
<div class="dte-toolbar__mobile-actions">
<button
type="button"
class="dte-mobile-btn"
v-tooltip.bottom="'Identificação do template'"
@click="openDrawer('form')"
>
<i class="pi pi-tag" />
<span>Identificação</span>
</button>
<button
type="button"
class="dte-mobile-btn"
v-tooltip.bottom="'Inserir variáveis'"
@click="openDrawer('vars')"
>
<i class="pi pi-code" />
<span>Variáveis</span>
</button>
</div>
<div class="dte-toolbar__title"> <div class="dte-toolbar__title">
<i class="pi pi-file-edit" /> <i class="pi pi-file-edit" />
<span>Conteúdo do documento</span> <span>Conteúdo do documento</span>
@@ -473,7 +705,8 @@ function onSave() {
<!-- EDITOR 3 colunas (form / editor / variáveis) --> <!-- EDITOR 3 colunas (form / editor / variáveis) -->
<div v-show="activeTab === 'editor'" class="dte-cols"> <div v-show="activeTab === 'editor'" class="dte-cols">
<!-- COL 1 (esquerda): Form de metadados --> <!-- COL 1 (esquerda): Form de metadados teleporta pro drawer no mobile -->
<Teleport to="#dte-mobile-drawer-form" :disabled="!isMobile">
<aside class="dte-side"> <aside class="dte-side">
<div class="dte-side__head"> <div class="dte-side__head">
<i class="pi pi-tag" /> <i class="pi pi-tag" />
@@ -498,6 +731,7 @@ function onSave() {
</div> </div>
</div> </div>
</aside> </aside>
</Teleport>
<!-- COL 2 (centro): Tabs Cabeçalho/Corpo/Rodapé + editor --> <!-- COL 2 (centro): Tabs Cabeçalho/Corpo/Rodapé + editor -->
<main class="dte-main"> <main class="dte-main">
@@ -544,7 +778,8 @@ function onSave() {
</div> </div>
</main> </main>
<!-- COL 3 (direita): Variáveis disponíveis --> <!-- COL 3 (direita): Variáveis disponíveis teleporta pro drawer no mobile -->
<Teleport to="#dte-mobile-drawer-vars" :disabled="!isMobile">
<aside class="dte-vars"> <aside class="dte-vars">
<div class="dte-vars__head"> <div class="dte-vars__head">
<i class="pi pi-code" /> <i class="pi pi-code" />
@@ -573,6 +808,7 @@ function onSave() {
</div> </div>
</div> </div>
</aside> </aside>
</Teleport>
</div> </div>
<!-- PREVIEW full width --> <!-- PREVIEW full width -->