Files
agenciapsilmno/src/layout/melissa/MelissaBloqueios.vue
T
Leonardo 9966b5f175 Melissa: paginas nativas cfg-* + temas + textos com fundo + drawer WA
CHROME COMPARTILHADO + 18 PAGINAS NATIVAS
- MelissaConfigPage: chrome unico (header, drawer mobile, sidebar com Configuracoes
  + FAQ slot, main com Suspense). Replica fake-dialog right rule e fica flush
  com o config-aside global.
- 18 wrappers finos (~25 linhas cada): cfg-precificacao, cfg-descontos,
  cfg-excecoes, cfg-convenios, cfg-wa, cfg-wa-pessoal, cfg-wa-oficial,
  cfg-wa-templates, cfg-conversas-tags/autoreply/optouts/sla/bots,
  cfg-lembretes, cfg-creditos-wa, cfg-sms, cfg-email-templates,
  cfg-recursos-extras, cfg-recursos-extras-extrato, cfg-auditoria.
- Cada wrapper usa defineAsyncComponent + Suspense pra evitar race com
  tenantStore no boot (loading travado em alguns chooser-style pages).
- MelissaLayout: imports + SECOES + MELISSA_NON_CONFIG_SLUGS + render
  conditions atualizados pra cobrir os 18 slugs.

PAGINAS LEGADAS DETECTAM CONTEXTO MELISSA
- ConfiguracoesWhatsappChooserPage, WhatsappPage, TwilioWhatsappPage,
  SmsPage, RecursosExtrasPage, EmailTemplatesPage, AddonsExtratoPage,
  AgendadorPage, ConversasAutoreplyPage: route.startsWith('/melissa')
  decide se router.push vai pro slug Melissa ou /configuracoes legado.
- Anchors <a href="/configuracoes/..."> (que recarregavam pagina e
  vazavam o usuario do Melissa) trocados por RouterLink context-aware.
- MelissaAgenda.goSettings agora vai pra /melissa/agenda-config.

PERSONALIZAR > TEMAS
- melissaThemes.js: catalogo Freud/Klein/Jung (wallpaper + cor primaria
  + preset Lara/Nora + surface).
- Toggle de tema aplica tudo de uma vez; persistido em melissa_prefs.themeName.
- Boot resolve themeName -> imagem via fetch + data URL (sem guardar
  data URL gigante no DB).
- onCustomFileChange/onClearBg invalidam themeName quando user mexe no bg.

PERSONALIZAR > FUNDO NOS TEXTOS
- Pref textBgEnabled em melissa_prefs.
- MelissaHeroClock: prop textBg envolve relogio/data/saudacao/resumo
  em <span class="hero-text"> que ganha bg branco/preto 60% + borda
  + padding + radius quando o toggle esta on.
- Vars --m-hero-text-bg / --m-hero-text-border flipam com light/dark.

TOP + DOCK COM GRADIENT HORIZONTAL
- Var --m-band: preto 80% (dark) / branco 80% (light).
- .melissa-topbar-band: gradiente cor->transparente (right->left) atras
  dos botoes do topo.
- .melissa-dock: gradiente cor->transparente (left->right) atras dos pins.

MELISSANEGOCIO ABSORVE MINHA EMPRESA
- Adiciona logo upload + preview "cartao de visita" (computeds
  enderecoLinhas/redesValidas/temDados/logoDisplay + redeIcon helper).
- Normaliza dados legados do cfg-empresa: redes_sociais.{rede} virou {name}.
- Preview teleporta entre 3 destinos baseado no viewport:
  mobile -> drawer; mid-desktop -> sidebar; wide-desktop (>=1340px) ->
  painel flutuante FORA do fake dialog (ancora no right edge + 14px gap,
  altura segue conteudo, header alinhado com header do dialog).
- Remove cfg-empresa de melissaConfigGrupos.js + COMPONENT_MAP do
  MelissaConfiguracoes; grupo "Empresa & Plataforma" -> "Plataforma".

CRONOMETRO -> SESSAO AGENDADA
- MelissaCronometro emite session-end ao parar com paciente selecionado
  (threshold 5s pra ignorar start/stop acidental).
- MelissaLayout.onCronometroSessionEnd busca agenda_eventos do paciente
  no dia (tipo='sessao'), pega o mais recente e grava em
  extra_fields.cronometro_duracao_seg + cronometro_parado_em.
- Toast: sucesso ("X min salvos") ou warn ("sessao nao encontrada").

CONVERSATIONDRAWER WHATSAPP-LIKE
- Nova imagem whatsapp-bg.jpg (renomeada de hash random) usada como
  tile (380px) no .cd-msgs.
- Light: bege #efeae2 + multiply blend.
- Dark: #0b141a + camada 78% sobre o doodle.
- Bubbles WA-style (verde out / branco-dark in com tails) ja existiam.

EXTRATO RECURSOS EXTRAS
- Filtros 2-por-linha em Melissa (vs 1/4 no /configuracoes).
- Cards de Resumo teleportam pro #cfg-page-side em Melissa
  (1-col empilhado no drawer; 4-col inline no /configuracoes).
- Botoes de exportar com flex-1 distribuidos em uma unica linha em
  desktop, wrap no mobile.
- DataTable scrollable em ambos os layouts.

OUTROS AJUSTES MENORES
- Cfg-conversas-autoreply: dias semana 4-cols em Melissa (vs 7-cols
  no /configuracoes).
- Cfg-creditos-wa: 1/2 por linha (vs 1/2/4) em Melissa.
- Cfg-recursos-extras: pacotes 1/2 (vs 1/2/4); "Em breve" 1-col.
- WhatsAppPage aba Templates: guia de formatacao teleporta pro side
  drawer em Melissa, deixando textareas full-width.
- ConfigPage chrome agora tem #cfg-page-actions target pros Teleport
  de acoes (refresh button etc).
- Imagens renomeadas em src/assets/themes/ (freudwebp/melainewebp/
  jungwebp.webp) e src/assets/whatsapp-bg.jpg.
- JoditTextEditor.vue novo (wrapper Jodit generico, sem features de email).
- MelissaConfigList.vue novo (lista compartilhada de configs pro drawer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:48:18 -03:00

1510 lines
53 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>
/*
* MelissaBloqueios — Pagina nativa Melissa pra "Bloqueios e Feriados".
*
* Substitui o embed cfg-bloqueios que vivia dentro do MelissaConfiguracoes.
* Layout 2-col:
* - COL 1 (sidebar) — Card "Resumo" (3 stats + ano nav) + Card
* "Acoes rapidas" (Novo bloqueio + Feriado municipal) + Card
* "Como funciona" (explicacao dos 3 tipos)
* - COL 2 (main) — 3 cards: Feriados Nacionais (read-only) +
* Feriados Municipais (CRUD) + Bloqueios (CRUD). Em desktop
* 50/50 com Bloqueios full-width abaixo.
*
* Logica espelhada do BloqueiosPage.vue (composable useFeriados +
* tabela agenda_bloqueios).
*/
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import MelissaConfigList from './MelissaConfigList.vue';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import { useFeriados } from '@/composables/useFeriados';
// DatePicker/Tag/Skeleton/Dialog: auto via PrimeVueResolver
const emit = defineEmits(['close']);
const toast = useToast();
const confirm = useConfirm();
const tenantStore = useTenantStore();
const {
nacionais,
municipais,
loading: loadingF,
load: loadFeriados,
criar: criarFeriado,
remover: removerFeriado,
isDuplicata
} = useFeriados();
// ── 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 (default) e lista de configs (alterna inline na sidebar)
const cfgOpen = ref(false);
function toggleCfg() { cfgOpen.value = !cfgOpen.value; }
function fecharCfg() { cfgOpen.value = false; }
// ── Estado ─────────────────────────────────────────────────
const loadingB = ref(true);
const saving = ref(false);
const ownerId = ref(null);
const tenantId = ref(null);
const ano = ref(new Date().getFullYear());
const bloqueios = ref([]);
// Dialog bloqueio
const dlgOpen = ref(false);
const dlgMode = ref('add');
const form = ref(emptyForm());
// Dialog feriado municipal
const fdlgOpen = ref(false);
const fsaving = ref(false);
const fform = ref(emptyFForm());
function emptyForm() {
return {
id: null,
titulo: '',
data_inicio: null,
data_fim: null,
hora_inicio: null,
hora_fim: null,
recorrente: false,
dia_semana: null,
observacao: ''
};
}
function emptyFForm() {
return { nome: '', data: null, observacao: '', bloqueia_sessoes: false };
}
// ── Helpers ────────────────────────────────────────────────
function dateToISO(d) {
if (!d) return null;
const dt = d instanceof Date ? d : new Date(d);
return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
}
function isoToDate(s) {
if (!s) return null;
const [y, m, d] = String(s).split('-').map(Number);
return new Date(y, m - 1, d);
}
function dateToHHMM(d) {
if (!d || !(d instanceof Date)) return null;
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
}
function hhmmToDate(hhmm) {
if (!hhmm) return null;
const [h, m] = String(hhmm).split(':').map(Number);
const d = new Date();
d.setHours(h, m, 0, 0);
return d;
}
function fmtDate(iso) {
if (!iso) return '—';
const [y, m, d] = String(iso).split('-');
return `${d}/${m}/${y}`;
}
function fmtDateShort(iso) {
if (!iso) return '';
const [, m, d] = String(iso).split('-');
return `${d}/${m}`;
}
function fmtHora(t) {
if (!t) return null;
return String(t).slice(0, 5);
}
function fmtPeriodo(b) {
if (b.recorrente && b.dia_semana != null) {
const dias = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
return `Toda ${dias[b.dia_semana]}`;
}
const ini = fmtDate(b.data_inicio);
if (!b.data_fim || b.data_fim === b.data_inicio) {
const hora = b.hora_inicio
? ` · ${fmtHora(b.hora_inicio)}${fmtHora(b.hora_fim) || '?'}`
: ' · Dia inteiro';
return ini + hora;
}
return `${ini} até ${fmtDate(b.data_fim)}`;
}
// ── Boot + load ────────────────────────────────────────────
async function boot() {
const { data } = await supabase.auth.getUser();
ownerId.value = data?.user?.id || null;
tenantId.value = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
if (tenantId.value) await loadFeriados(tenantId.value);
await loadBloqueios();
}
async function loadBloqueios() {
if (!ownerId.value) return;
loadingB.value = true;
try {
const { data, error } = await supabase
.from('agenda_bloqueios')
.select('*')
.eq('owner_id', ownerId.value)
.gte('data_inicio', `${ano.value}-01-01`)
.lte('data_inicio', `${ano.value}-12-31`)
.order('data_inicio');
if (error) throw error;
bloqueios.value = data || [];
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3500 });
} finally {
loadingB.value = false;
}
}
async function anoAnterior() {
ano.value--;
if (tenantId.value) await loadFeriados(tenantId.value, ano.value);
await loadBloqueios();
}
async function anoProximo() {
ano.value++;
if (tenantId.value) await loadFeriados(tenantId.value, ano.value);
await loadBloqueios();
}
// ── Feriado municipal CRUD ─────────────────────────────────
function abrirFeriadoMunicipal() {
fform.value = emptyFForm();
fdlgOpen.value = true;
}
const fformValid = computed(() => !!fform.value.nome.trim() && !!fform.value.data);
const fformDuplicate = computed(() => {
if (!fform.value.data || !fform.value.nome) return false;
return isDuplicata(dateToISO(fform.value.data), fform.value.nome);
});
async function salvarFeriado() {
if (!fformValid.value) return;
const iso = dateToISO(fform.value.data);
if (isDuplicata(iso, fform.value.nome)) {
toast.add({
severity: 'warn',
summary: 'Duplicado',
detail: 'Já existe um feriado com esse nome nessa data.',
life: 3000
});
return;
}
fsaving.value = true;
try {
await criarFeriado({
tenant_id: tenantId.value,
owner_id: ownerId.value,
tipo: 'municipal',
nome: fform.value.nome.trim(),
data: iso,
observacao: fform.value.observacao || null,
bloqueia_sessoes: fform.value.bloqueia_sessoes
});
toast.add({ severity: 'success', summary: 'Feriado cadastrado', life: 1800 });
fdlgOpen.value = false;
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3500 });
} finally {
fsaving.value = false;
}
}
function excluirFeriado(id) {
confirm.require({
message: 'Deseja remover este feriado municipal?',
header: 'Confirmar remoção',
icon: 'pi pi-exclamation-triangle',
rejectLabel: 'Cancelar',
acceptLabel: 'Remover',
acceptClass: 'p-button-danger',
accept: async () => {
try {
await removerFeriado(id);
toast.add({ severity: 'success', summary: 'Removido', life: 1500 });
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 });
}
}
});
}
// ── Bloqueio CRUD ──────────────────────────────────────────
function abrirAddBloqueio() {
form.value = emptyForm();
dlgMode.value = 'add';
dlgOpen.value = true;
}
function abrirEditBloqueio(b) {
form.value = {
id: b.id,
titulo: b.titulo,
data_inicio: isoToDate(b.data_inicio),
data_fim: isoToDate(b.data_fim),
hora_inicio: hhmmToDate(fmtHora(b.hora_inicio)),
hora_fim: hhmmToDate(fmtHora(b.hora_fim)),
recorrente: !!b.recorrente,
dia_semana: b.dia_semana ?? null,
observacao: b.observacao || ''
};
dlgMode.value = 'edit';
dlgOpen.value = true;
}
const formValid = computed(() => !!form.value.titulo.trim() && !!form.value.data_inicio);
async function salvarBloqueio() {
if (!formValid.value) return;
saving.value = true;
try {
const payload = {
owner_id: ownerId.value,
tenant_id: tenantId.value,
tipo: 'bloqueio',
titulo: form.value.titulo.trim(),
data_inicio: dateToISO(form.value.data_inicio),
data_fim: dateToISO(form.value.data_fim) || null,
hora_inicio: dateToHHMM(form.value.hora_inicio) || null,
hora_fim: dateToHHMM(form.value.hora_fim) || null,
recorrente: form.value.recorrente,
dia_semana: form.value.dia_semana ?? null,
observacao: form.value.observacao || null,
origem: 'manual'
};
if (dlgMode.value === 'edit') {
const { error } = await supabase
.from('agenda_bloqueios')
.update(payload)
.eq('id', form.value.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Bloqueio atualizado.', life: 1800 });
} else {
const { error } = await supabase.from('agenda_bloqueios').insert(payload);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Criado', detail: 'Bloqueio adicionado.', life: 1800 });
}
dlgOpen.value = false;
await loadBloqueios();
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3500 });
} finally {
saving.value = false;
}
}
function excluirBloqueio(id) {
const b = bloqueios.value.find((x) => x.id === id);
confirm.require({
message: b?.titulo ? `Remover o bloqueio "${b.titulo}"?` : 'Deseja remover este bloqueio?',
header: 'Confirmar remoção',
icon: 'pi pi-exclamation-triangle',
rejectLabel: 'Cancelar',
acceptLabel: 'Remover',
acceptClass: 'p-button-danger',
accept: async () => {
try {
const { error } = await supabase
.from('agenda_bloqueios')
.delete()
.eq('id', id);
if (error) throw error;
bloqueios.value = bloqueios.value.filter((b) => b.id !== id);
toast.add({ severity: 'success', summary: 'Removido', life: 1500 });
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 });
}
}
});
}
const loading = computed(() => loadingF.value || loadingB.value);
// ── 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();
tenantId.value = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
await boot();
});
onBeforeUnmount(() => {
if (_mqMobile) {
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
catch { _mqMobile.removeListener(_onMqMobileChange); }
}
});
</script>
<template>
<Transition name="mbq-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="mbq-mobile-drawer"
:class="{ 'is-open': drawerOpen }"
>
<div id="mbq-mobile-drawer-target" class="mbq-mobile-drawer__scroll" />
</div>
</Transition>
<Transition name="mbq-drawer-fade">
<div
v-show="isMobile && drawerOpen"
class="mbq-mobile-drawer__backdrop"
@click="fecharDrawer"
/>
</Transition>
<ConfirmDialog />
<section class="mbq-page">
<header class="mbq-page__head">
<button
class="mbq-menu-btn mbq-menu-btn--mobile-only"
v-tooltip.bottom="'Resumo & Acoes'"
@click="toggleDrawer"
>
<i class="pi pi-bars" />
<span>Menu</span>
</button>
<div class="mbq-page__title">
<i class="pi pi-ban mbq-page__title-icon" />
<span>Bloqueios e Feriados</span>
<span class="mbq-page__year">{{ ano }}</span>
</div>
<div class="mbq-page__actions">
<button
class="mbq-act-btn"
v-tooltip.bottom="'Atualizar'"
:disabled="loading"
@click="boot"
>
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
</button>
<button class="mbq-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
<i class="pi pi-times" />
</button>
</div>
</header>
<div class="mbq-subheader">
<i class="pi pi-info-circle mbq-subheader__icon" />
<span class="mbq-subheader__text">
Feriados nacionais sao automaticos. Adicione feriados municipais
e periodos bloqueados (ferias, recesso, licencas) que afetam
a sua agenda.
</span>
</div>
<div class="mbq-body">
<Teleport to="#mbq-mobile-drawer-target" :disabled="!isMobile">
<aside class="mbq-side">
<button class="mbq-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 mbq-cfg-btn__chev" />
</button>
<div v-if="cfgOpen" class="mbq-side__scroll mbq-side__scroll--cfg">
<MelissaConfigList @select="fecharCfg" />
</div>
<div v-else class="mbq-side__scroll">
<!-- Card: Resumo -->
<div class="mbq-w mbq-w--side">
<div class="mbq-w__head">
<div class="mbq-w__icon"><i class="pi pi-chart-pie" /></div>
<div class="mbq-w__title">
<div class="mbq-w__title-text">Resumo</div>
<div class="mbq-w__sub">Quantidades e ano</div>
</div>
</div>
<div class="mbq-w__body">
<div class="mbq-stats">
<div class="mbq-stat mbq-stat--blue">
<div class="mbq-stat__val">{{ nacionais.length }}</div>
<div class="mbq-stat__lbl">Nacionais</div>
</div>
<div class="mbq-stat mbq-stat--orange">
<div class="mbq-stat__val">{{ municipais.length }}</div>
<div class="mbq-stat__lbl">Municipais</div>
</div>
<div class="mbq-stat mbq-stat--red">
<div class="mbq-stat__val">{{ bloqueios.length }}</div>
<div class="mbq-stat__lbl">Bloqueios</div>
</div>
</div>
<div class="mbq-yearnav">
<button class="mbq-btn mbq-btn--icon" :disabled="loading" @click="anoAnterior">
<i class="pi pi-chevron-left" />
</button>
<span class="mbq-yearnav__label">{{ ano }}</span>
<button class="mbq-btn mbq-btn--icon" :disabled="loading" @click="anoProximo">
<i class="pi pi-chevron-right" />
</button>
</div>
</div>
</div>
<!-- Card: Acoes -->
<div class="mbq-w mbq-w--side">
<div class="mbq-w__head">
<div class="mbq-w__icon"><i class="pi pi-plus" /></div>
<div class="mbq-w__title">
<div class="mbq-w__title-text">Adicionar</div>
<div class="mbq-w__sub">Feriado municipal ou bloqueio</div>
</div>
</div>
<div class="mbq-w__body">
<button class="mbq-btn mbq-btn--full" @click="abrirFeriadoMunicipal">
<i class="pi pi-map-marker" />
<span>Feriado municipal</span>
</button>
<button class="mbq-btn mbq-btn--primary mbq-btn--full" @click="abrirAddBloqueio">
<i class="pi pi-ban" />
<span>Novo bloqueio</span>
</button>
</div>
</div>
<!-- Card: Como funciona -->
<div class="mbq-w mbq-w--side">
<div class="mbq-w__head">
<div class="mbq-w__icon"><i class="pi pi-question-circle" /></div>
<div class="mbq-w__title">
<div class="mbq-w__title-text">Como funciona</div>
<div class="mbq-w__sub">Tipos de impedimento</div>
</div>
</div>
<div class="mbq-w__body">
<ul class="mbq-faq">
<li class="mbq-faq__item">
<span class="mbq-faq__bullet" style="background: #3b82f6" />
<div>
<strong>Nacionais:</strong>
gerados automaticamente (Carnaval, Pascoa, Natal etc).
</div>
</li>
<li class="mbq-faq__item">
<span class="mbq-faq__bullet" style="background: #f97316" />
<div>
<strong>Municipais:</strong>
voce cadastra (aniversario da cidade, padroeiro etc).
</div>
</li>
<li class="mbq-faq__item">
<span class="mbq-faq__bullet" style="background: #ef4444" />
<div>
<strong>Bloqueios:</strong>
ferias, recesso, licenca, ou janela horaria especifica.
Pode ser recorrente.
</div>
</li>
</ul>
</div>
</div>
</div>
</aside>
</Teleport>
<div class="mbq-main">
<!-- Loading -->
<template v-if="loading">
<div class="mbq-w" v-for="n in 3" :key="`sk-${n}`">
<div class="mbq-w__body">
<Skeleton width="40%" height="20px" class="mb-3" />
<Skeleton v-for="m in 4" :key="`sk-${n}-${m}`" width="100%" height="32px" class="mb-2" />
</div>
</div>
</template>
<template v-else>
<!-- Feriados Nacionais -->
<div class="mbq-w">
<div class="mbq-w__head">
<div class="mbq-w__icon mbq-w__icon--blue"><i class="pi pi-flag" /></div>
<div class="mbq-w__title">
<div class="mbq-w__title-text">Feriados Nacionais</div>
<div class="mbq-w__sub">Gerados automaticamente</div>
</div>
<span v-if="nacionais.length" class="mbq-w__count">{{ nacionais.length }}</span>
</div>
<div class="mbq-w__body">
<div v-if="!nacionais.length" class="mbq-empty">
<i class="pi pi-info-circle" />
<span>Nenhum feriado nacional para {{ ano }}.</span>
</div>
<div v-else class="mbq-list">
<div
v-for="f in nacionais"
:key="f.data + f.nome"
class="mbq-item mbq-item--nat"
>
<div class="mbq-item__date">{{ fmtDateShort(f.data) }}</div>
<div class="mbq-item__title">{{ f.nome }}</div>
<Tag
v-if="f.movel"
value="Móvel"
severity="secondary"
class="mbq-item__tag"
/>
</div>
</div>
</div>
</div>
<!-- Feriados Municipais -->
<div class="mbq-w">
<div class="mbq-w__head">
<div class="mbq-w__icon mbq-w__icon--orange"><i class="pi pi-map-marker" /></div>
<div class="mbq-w__title">
<div class="mbq-w__title-text">Feriados Municipais</div>
<div class="mbq-w__sub">Cadastrados manualmente</div>
</div>
<button class="mbq-w__action" @click="abrirFeriadoMunicipal">
<i class="pi pi-plus" />
<span>Adicionar</span>
</button>
</div>
<div class="mbq-w__body">
<div v-if="!municipais.length" class="mbq-empty">
<i class="pi pi-map-marker" />
<span>Nenhum feriado municipal cadastrado para {{ ano }}.</span>
</div>
<div v-else class="mbq-list">
<div
v-for="f in municipais"
:key="f.id"
class="mbq-item mbq-item--mun"
>
<div class="mbq-item__date">{{ fmtDate(f.data) }}</div>
<div class="mbq-item__title">
{{ f.nome }}
<span v-if="f.observacao" class="mbq-item__obs"> {{ f.observacao }}</span>
</div>
<button
class="mbq-icon-btn mbq-icon-btn--danger"
v-tooltip.left="'Remover'"
@click="excluirFeriado(f.id)"
>
<i class="pi pi-trash" />
</button>
</div>
</div>
</div>
</div>
<!-- Bloqueios -->
<div class="mbq-w mbq-w--full">
<div class="mbq-w__head">
<div class="mbq-w__icon mbq-w__icon--red"><i class="pi pi-ban" /></div>
<div class="mbq-w__title">
<div class="mbq-w__title-text">Bloqueios</div>
<div class="mbq-w__sub">Férias, recesso, licença ou janela horária</div>
</div>
<button class="mbq-w__action" @click="abrirAddBloqueio">
<i class="pi pi-plus" />
<span>Novo</span>
</button>
</div>
<div class="mbq-w__body">
<div v-if="!bloqueios.length" class="mbq-empty">
<i class="pi pi-ban" />
<span>Nenhum bloqueio cadastrado para {{ ano }}.</span>
</div>
<div v-else class="mbq-list">
<div
v-for="b in bloqueios"
:key="b.id"
class="mbq-item mbq-item--blk"
>
<div class="mbq-item__date">{{ fmtPeriodo(b) }}</div>
<div class="mbq-item__title">
{{ b.titulo }}
<Tag
v-if="b.recorrente"
value="Recorrente"
severity="warn"
class="mbq-item__tag"
/>
<span v-if="b.observacao" class="mbq-item__obs"> {{ b.observacao }}</span>
</div>
<div class="mbq-item__actions">
<button
class="mbq-icon-btn"
v-tooltip.top="'Editar'"
@click="abrirEditBloqueio(b)"
>
<i class="pi pi-pencil" />
</button>
<button
class="mbq-icon-btn mbq-icon-btn--danger"
v-tooltip.top="'Remover'"
@click="excluirBloqueio(b.id)"
>
<i class="pi pi-trash" />
</button>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<!-- Dialog feriado municipal -->
<Dialog
v-model:visible="fdlgOpen"
modal
:draggable="false"
pt:mask:class="backdrop-blur-xs"
header="Cadastrar feriado municipal"
:style="{ width: '460px', maxWidth: '95vw' }"
>
<div class="mbq-dlg">
<div class="mbq-dlg__field">
<label class="mbq-dlg__label">Nome do feriado *</label>
<InputText
v-model="fform.nome"
class="w-full"
placeholder="Ex.: Aniversário da cidade, Padroeiro…"
/>
</div>
<div class="mbq-dlg__field">
<label class="mbq-dlg__label">Data *</label>
<DatePicker
v-model="fform.data"
showIcon
fluid
iconDisplay="input"
dateFormat="dd/mm/yy"
:manualInput="false"
>
<template #inputicon="sp">
<i class="pi pi-calendar" @click="sp.clickCallback" />
</template>
</DatePicker>
</div>
<div class="mbq-dlg__field">
<label class="mbq-dlg__label">Observação <span class="mbq-dlg__optional">(opcional)</span></label>
<Textarea
v-model="fform.observacao"
class="w-full"
rows="2"
autoResize
placeholder="Nota interna…"
/>
</div>
<div v-if="fformDuplicate" class="mbq-dlg__warn">
<i class="pi pi-exclamation-triangle" />
existe um feriado com esse nome nessa data.
</div>
</div>
<template #footer>
<button class="mbq-btn" @click="fdlgOpen = false">
<span>Cancelar</span>
</button>
<button
class="mbq-btn mbq-btn--primary"
:disabled="!fformValid || fformDuplicate"
@click="salvarFeriado"
>
<i :class="fsaving ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
<span>Cadastrar</span>
</button>
</template>
</Dialog>
<!-- Dialog bloqueio -->
<Dialog
v-model:visible="dlgOpen"
modal
:draggable="false"
pt:mask:class="backdrop-blur-xs"
:header="dlgMode === 'edit' ? 'Editar bloqueio' : 'Novo bloqueio'"
:style="{ width: '500px', maxWidth: '95vw' }"
>
<div class="mbq-dlg">
<div class="mbq-dlg__field">
<label class="mbq-dlg__label">Título *</label>
<InputText
v-model="form.titulo"
class="w-full"
placeholder="Ex.: Recesso, Férias, Licença…"
/>
</div>
<div class="mbq-dlg__row">
<div class="mbq-dlg__field">
<label class="mbq-dlg__label">Data início *</label>
<DatePicker
v-model="form.data_inicio"
showIcon
fluid
iconDisplay="input"
dateFormat="dd/mm/yy"
:manualInput="false"
>
<template #inputicon="sp">
<i class="pi pi-calendar" @click="sp.clickCallback" />
</template>
</DatePicker>
</div>
<div class="mbq-dlg__field">
<label class="mbq-dlg__label">Data fim <span class="mbq-dlg__optional">(opcional)</span></label>
<DatePicker
v-model="form.data_fim"
showIcon
fluid
iconDisplay="input"
dateFormat="dd/mm/yy"
:manualInput="false"
:minDate="form.data_inicio || undefined"
>
<template #inputicon="sp">
<i class="pi pi-calendar" @click="sp.clickCallback" />
</template>
</DatePicker>
</div>
</div>
<div class="mbq-dlg__row">
<div class="mbq-dlg__field">
<label class="mbq-dlg__label">Hora início <span class="mbq-dlg__optional">(opcional)</span></label>
<DatePicker
v-model="form.hora_inicio"
showIcon
fluid
iconDisplay="input"
timeOnly
hourFormat="24"
:stepMinute="15"
:manualInput="false"
>
<template #inputicon="sp">
<i class="pi pi-clock" @click="sp.clickCallback" />
</template>
</DatePicker>
</div>
<div class="mbq-dlg__field">
<label class="mbq-dlg__label">Hora fim <span class="mbq-dlg__optional">(opcional)</span></label>
<DatePicker
v-model="form.hora_fim"
showIcon
fluid
iconDisplay="input"
timeOnly
hourFormat="24"
:stepMinute="15"
:manualInput="false"
>
<template #inputicon="sp">
<i class="pi pi-clock" @click="sp.clickCallback" />
</template>
</DatePicker>
</div>
</div>
<div class="mbq-dlg__field">
<label class="mbq-dlg__label">Observação <span class="mbq-dlg__optional">(opcional)</span></label>
<Textarea
v-model="form.observacao"
class="w-full"
rows="2"
autoResize
/>
</div>
</div>
<template #footer>
<button class="mbq-btn" @click="dlgOpen = false">
<span>Cancelar</span>
</button>
<button
class="mbq-btn mbq-btn--primary"
:disabled="!formValid"
@click="salvarBloqueio"
>
<i :class="saving ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
<span>{{ dlgMode === 'edit' ? 'Salvar' : 'Adicionar' }}</span>
</button>
</template>
</Dialog>
</section>
</template>
<style scoped>
/* ═══════ Page chrome ═══════ */
.mbq-page {
position: absolute;
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) var(--m-config-aside-left, 6px);
z-index: 40;
display: flex;
flex-direction: column;
background: var(--m-bg-medium);
backdrop-filter: blur(32px) saturate(160%);
-webkit-backdrop-filter: blur(32px) saturate(160%);
border: 1px solid var(--m-border);
border-radius: 18px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
overflow: hidden;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
color: var(--m-text);
animation: mbq-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
@keyframes mbq-page-enter {
from { opacity: 0; transform: scale(0.985); }
to { opacity: 1; transform: scale(1); }
}
.mbq-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;
}
.mbq-page__title {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
font-weight: 700;
color: var(--m-text);
}
.mbq-page__title-icon { color: var(--p-primary-color); font-size: 1.05rem; }
.mbq-page__year {
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;
font-variant-numeric: tabular-nums;
}
.mbq-page__actions { display: flex; align-items: center; gap: 6px; }
.mbq-act-btn {
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;
}
.mbq-act-btn:hover { background: var(--m-bg-soft-hover); color: var(--m-text); }
.mbq-act-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.mbq-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;
}
.mbq-close:hover { background: var(--m-bg-soft-hover); color: var(--m-text); }
.mbq-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;
}
.mbq-menu-btn:hover { background: var(--m-bg-soft-hover); }
.mbq-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;
}
.mbq-subheader__icon {
color: var(--p-primary-color);
font-size: 0.85rem;
margin-top: 2px;
flex-shrink: 0;
}
/* ═══════ Body 2-col ═══════ */
.mbq-body {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
.mbq-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;
}
.mbq-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;
}
.mbq-side__scroll::-webkit-scrollbar { width: 5px; }
.mbq-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
/* Botao "Configuracoes" no topo da .mbq-side. Click alterna entre
cards (default) e lista de configs (estado is-open: vira "Voltar"). */
.mbq-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;
}
.mbq-cfg-btn:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mbq-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);
}
.mbq-cfg-btn > i:first-child {
color: var(--p-primary-color);
font-size: 0.92rem;
}
.mbq-cfg-btn > span { flex: 1; }
.mbq-cfg-btn__chev {
color: var(--m-text-muted);
font-size: 0.7rem;
}
.mbq-side__scroll--cfg {
padding: 8px;
gap: 0;
}
.mbq-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;
}
.mbq-main::-webkit-scrollbar { width: 5px; }
.mbq-main::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
/* Desktop (>=1024): main em grid 2-col, Bloqueios spans 2 cols.
Cards min-h 300 + max-h 100% + body scroll (lições aprendidas). */
@media (min-width: 1024px) {
.mbq-main {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
align-items: start;
align-content: start;
}
.mbq-main > .mbq-w {
min-height: 300px;
max-height: 100%;
display: flex;
flex-direction: column;
}
.mbq-main > .mbq-w--full { grid-column: 1 / -1; }
.mbq-side > .mbq-side__scroll > .mbq-w--side {
min-height: 300px;
max-height: 100%;
display: flex;
flex-direction: column;
}
.mbq-main > .mbq-w > .mbq-w__body,
.mbq-side .mbq-w--side > .mbq-w__body {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mbq-main > .mbq-w > .mbq-w__body::-webkit-scrollbar,
.mbq-side .mbq-w--side > .mbq-w__body::-webkit-scrollbar { width: 5px; }
.mbq-main > .mbq-w > .mbq-w__body::-webkit-scrollbar-thumb,
.mbq-side .mbq-w--side > .mbq-w__body::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
}
/* ═══════ Card-base ═══════ */
.mbq-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;
}
.mbq-w--side {
background: var(--m-bg-medium);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
}
.mbq-w__head {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid var(--m-border);
}
.mbq-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);
}
.mbq-w__icon > i { font-size: 0.95rem; }
.mbq-w__icon--blue {
background: color-mix(in srgb, #3b82f6 15%, transparent);
color: #3b82f6;
}
.mbq-w__icon--orange {
background: color-mix(in srgb, #f97316 15%, transparent);
color: #f97316;
}
.mbq-w__icon--red {
background: color-mix(in srgb, #ef4444 15%, transparent);
color: #ef4444;
}
.mbq-w__title { flex: 1; min-width: 0; }
.mbq-w__title-text {
font-size: 0.92rem;
font-weight: 700;
color: var(--m-text);
line-height: 1.2;
}
.mbq-w__sub {
font-size: 0.74rem;
color: var(--m-text-muted);
margin-top: 2px;
line-height: 1.3;
}
.mbq-w__count {
flex-shrink: 0;
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;
}
.mbq-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;
}
.mbq-w__action:hover { background: var(--m-bg-soft-hover); }
.mbq-w__action > i { font-size: 0.72rem; color: var(--p-primary-color); }
.mbq-w__body {
padding: 14px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* ═══════ Sidebar: Resumo (mini stats) ═══════ */
.mbq-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.mbq-stat {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 10px 6px;
border-radius: 9px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
text-align: center;
}
.mbq-stat__val {
font-size: 1.2rem;
font-weight: 800;
line-height: 1.1;
}
.mbq-stat__lbl {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--m-text-muted);
}
.mbq-stat--blue .mbq-stat__val { color: #3b82f6; }
.mbq-stat--orange .mbq-stat__val { color: #f97316; }
.mbq-stat--red .mbq-stat__val { color: #ef4444; }
.mbq-yearnav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.mbq-yearnav__label {
flex: 1;
text-align: center;
font-size: 1.05rem;
font-weight: 800;
color: var(--m-text);
font-variant-numeric: tabular-nums;
}
/* ═══════ Sidebar: FAQ ═══════ */
.mbq-faq {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.mbq-faq__item {
display: flex;
gap: 10px;
font-size: 0.78rem;
color: var(--m-text-muted);
line-height: 1.4;
}
.mbq-faq__item > div { flex: 1; min-width: 0; }
.mbq-faq__item strong { color: var(--m-text); font-weight: 700; }
.mbq-faq__bullet {
width: 8px;
height: 8px;
border-radius: 50%;
margin-top: 6px;
flex-shrink: 0;
}
/* ═══════ Buttons ═══════ */
.mbq-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-soft);
color: var(--m-text);
cursor: pointer;
font-size: 0.82rem;
font-weight: 600;
font-family: inherit;
transition: background-color 120ms ease, opacity 120ms ease;
}
.mbq-btn:hover { background: var(--m-bg-soft-hover); }
.mbq-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.mbq-btn--primary {
background: var(--p-primary-color);
border-color: var(--p-primary-color);
color: var(--p-primary-contrast-color, #fff);
}
.mbq-btn--primary:hover {
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
}
.mbq-btn--full { width: 100%; }
.mbq-btn--icon {
width: 32px;
height: 32px;
padding: 0;
flex-shrink: 0;
}
.mbq-icon-btn {
width: 28px;
height: 28px;
display: grid;
place-items: center;
background: transparent;
border: 1px solid var(--m-border);
border-radius: 6px;
color: var(--m-text-muted);
cursor: pointer;
flex-shrink: 0;
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.mbq-icon-btn:hover {
background: var(--m-bg-soft-hover);
color: var(--m-text);
}
.mbq-icon-btn--danger:hover {
background: rgba(220, 38, 38, 0.12);
color: rgb(220, 38, 38);
border-color: rgba(220, 38, 38, 0.40);
}
/* ═══════ Lists / Items ═══════ */
.mbq-empty {
display: flex;
align-items: center;
gap: 8px;
padding: 14px;
border-radius: 9px;
background: var(--m-bg-medium);
border: 1px dashed var(--m-border);
color: var(--m-text-muted);
font-size: 0.82rem;
}
.mbq-empty > i { color: var(--p-primary-color); font-size: 0.92rem; }
.mbq-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.mbq-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 8px;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
font-size: 0.82rem;
flex-wrap: wrap;
transition: background-color 120ms ease;
}
.mbq-item:hover { background: var(--m-bg-soft-hover); }
.mbq-item--nat { border-left: 3px solid #3b82f6; }
.mbq-item--mun { border-left: 3px solid #f97316; }
.mbq-item--blk { border-left: 3px solid #ef4444; }
.mbq-item__date {
font-size: 0.74rem;
color: var(--m-text-muted);
font-weight: 600;
white-space: nowrap;
min-width: 80px;
font-variant-numeric: tabular-nums;
}
.mbq-item__title {
flex: 1;
min-width: 0;
font-weight: 600;
color: var(--m-text);
display: inline-flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.mbq-item__obs {
font-size: 0.72rem;
font-weight: 400;
color: var(--m-text-muted);
font-style: italic;
}
.mbq-item__tag {
font-size: 0.66rem !important;
flex-shrink: 0;
}
.mbq-item__actions {
display: flex;
gap: 4px;
margin-left: auto;
flex-shrink: 0;
}
/* ═══════ Dialog ═══════ */
.mbq-dlg {
display: flex;
flex-direction: column;
gap: 14px;
padding-top: 4px;
}
.mbq-dlg__row {
display: flex;
gap: 12px;
}
.mbq-dlg__row > .mbq-dlg__field { flex: 1; min-width: 0; }
.mbq-dlg__field {
display: flex;
flex-direction: column;
gap: 6px;
}
.mbq-dlg__label {
font-size: 0.78rem;
font-weight: 700;
color: var(--m-text);
}
.mbq-dlg__optional {
font-weight: 400;
color: var(--m-text-muted);
opacity: 0.7;
}
.mbq-dlg__warn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border-radius: 7px;
background: rgba(220, 38, 38, 0.10);
border: 1px solid rgba(220, 38, 38, 0.30);
color: rgb(220, 38, 38);
font-size: 0.78rem;
}
/* ═══════ Mobile drawer ═══════ */
.mbq-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;
}
.mbq-mobile-drawer.is-open { transform: translateX(0); }
.mbq-mobile-drawer__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;
}
.mbq-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
.mbq-mobile-drawer__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
/* No mobile a .mbq-side e teleportada pra dentro do drawer scroll. */
.mbq-mobile-drawer__scroll .mbq-side {
width: 100%;
border-right: none;
}
.mbq-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;
}
.mbq-drawer-fade-enter-active,
.mbq-drawer-fade-leave-active { transition: opacity 200ms ease; }
.mbq-drawer-fade-enter-from,
.mbq-drawer-fade-leave-to { opacity: 0; }
/* ═══════ Mobile (<1024px) ═══════ */
@media (max-width: 1023px) {
.mbq-body { flex-direction: column; padding: 0; }
.mbq-body > .mbq-side { display: none; }
.mbq-main { width: 100%; padding: 8px; }
.mbq-main .mbq-w {
height: auto;
flex: 0 0 auto;
align-self: stretch;
}
.mbq-page__title > span:first-of-type { display: none; }
.mbq-page__title-icon { display: none; }
.mbq-menu-btn--mobile-only { display: inline-flex; }
.mbq-dlg__row { flex-direction: column; gap: 14px; }
.mbq-item { padding: 10px; }
}
</style>