02af119dc6
Refator do mobile drawer em todas as Melissa Pages com sidebar: scroll move pra dentro de .xx-side__scroll (flex: 1 + min-height: 0) e o __footer vira flex-shrink: 0 last child de flex column. Espelha o pattern do AppMenu/layout-sidebar Rail. Substitui o sticky/margin:auto que falhava quando o conteudo era pequeno (deixava espaco vazio sob o "Limpar filtros"). Pages: Compromissos, Conversas, Documentos, FinanceiroLancamentos, Grupos, Medicos, Notificacoes, Pacientes, Recorrencias, Relatorios, Tags. Pacientes (caso especial): mp-quick fixo no topo (max-height: 50%) + mp-side flex: 1 com scroll/footer interno. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1293 lines
44 KiB
Vue
1293 lines
44 KiB
Vue
<script setup>
|
|
/*
|
|
* MelissaConversas — CRM de WhatsApp dentro de Melissa.
|
|
* Segue o blueprint melissa-page-blueprint.md.
|
|
*
|
|
* Layout 2-col:
|
|
* - COL 1 — Aside (~280px): filtros rápidos (todas/não lidas) +
|
|
* atribuição (minhas/não atribuídas) + canais + kanban resumo
|
|
* + alerta de não vinculados
|
|
* - COL 2 — Main: kanban grid 4-col (Urgente / Aguardando resposta /
|
|
* Aguardando paciente / Resolvido) com cards
|
|
*
|
|
* Click num card abre o ConversationDrawer (componente existente,
|
|
* já mountado no MelissaLayout). Sem rotas externas — fica tudo
|
|
* dentro do Melissa.
|
|
*/
|
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
|
import { supabase } from '@/lib/supabase/client';
|
|
import Popover from 'primevue/popover';
|
|
import { useConversations } from '@/composables/useConversations';
|
|
import { useConversationTags } from '@/composables/useConversationTags';
|
|
import { useTenantStore } from '@/stores/tenantStore';
|
|
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
|
// Badge/InputText/IconField/InputIcon: auto-import via PrimeVueResolver
|
|
|
|
const emit = defineEmits(['close']);
|
|
|
|
const tenantStore = useTenantStore();
|
|
const drawerStore = useConversationDrawerStore();
|
|
|
|
// ── Breakpoints + drawer (blueprint §2/§3) ─────────────────
|
|
const drawerOpen = ref(false);
|
|
const isMobile = ref(false);
|
|
const isCompact = ref(false);
|
|
let _mqMobile = null;
|
|
let _mqCompact = null;
|
|
function _onMqMobileChange(e) {
|
|
isMobile.value = e.matches;
|
|
if (!e.matches) drawerOpen.value = false;
|
|
}
|
|
function _onMqCompactChange(e) {
|
|
isCompact.value = e.matches;
|
|
}
|
|
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
|
|
function fecharDrawer() { drawerOpen.value = false; }
|
|
|
|
// ── Composables (espelhado da CRMConversasPage) ─────────────
|
|
const { threads, filteredThreads, byKanban, summary, filters, loading, load, subscribeRealtime, unsubscribeRealtime } = useConversations();
|
|
|
|
const tagsApi = useConversationTags();
|
|
const threadTagsMap = ref(new Map());
|
|
const tagById = computed(() => {
|
|
const m = {};
|
|
for (const t of tagsApi.allTags.value) m[t.id] = t;
|
|
return m;
|
|
});
|
|
function tagsForThread(threadKey) {
|
|
const ids = threadTagsMap.value.get(threadKey) || [];
|
|
return ids.map((id) => tagById.value[id]).filter(Boolean);
|
|
}
|
|
async function reloadThreadTags() {
|
|
const keys = filteredThreads.value.map((t) => t.thread_key);
|
|
threadTagsMap.value = await tagsApi.loadForThreads(keys);
|
|
}
|
|
|
|
// Atribuição
|
|
const currentUserId = ref(null);
|
|
supabase.auth.getUser().then(({ data }) => { currentUserId.value = data?.user?.id ?? null; });
|
|
const mineCount = computed(() => {
|
|
const uid = currentUserId.value;
|
|
if (!uid) return 0;
|
|
return threads.value.filter((t) => t.assigned_to === uid).length;
|
|
});
|
|
const unassignedCount = computed(() => threads.value.filter((t) => !t.assigned_to).length);
|
|
|
|
const memberNameMap = ref({});
|
|
async function loadMemberNames() {
|
|
const tenantId = tenantStore.activeTenantId;
|
|
if (!tenantId) return;
|
|
const { data } = await supabase
|
|
.from('v_tenant_members_with_profiles')
|
|
.select('user_id, full_name, email')
|
|
.eq('tenant_id', tenantId)
|
|
.eq('status', 'active');
|
|
const map = {};
|
|
for (const m of (data || [])) {
|
|
map[m.user_id] = m.full_name || m.email || '';
|
|
}
|
|
memberNameMap.value = map;
|
|
}
|
|
function assigneeLabel(userId) {
|
|
if (!userId) return '';
|
|
const full = memberNameMap.value[userId];
|
|
if (!full) return 'Atribuída';
|
|
const parts = full.trim().split(/\s+/);
|
|
if (parts.length === 1) return parts[0].slice(0, 14);
|
|
return `${parts[0]} ${parts[parts.length - 1][0]}.`;
|
|
}
|
|
|
|
// Constantes
|
|
const KANBAN_COLUMNS = [
|
|
{ key: 'urgent', label: 'Urgente', icon: 'pi pi-exclamation-triangle', color: 'red' },
|
|
{ key: 'awaiting_us', label: 'Aguardando resposta', icon: 'pi pi-inbox', color: 'amber' },
|
|
{ key: 'awaiting_patient', label: 'Aguardando paciente', icon: 'pi pi-hourglass', color: 'blue' },
|
|
{ key: 'resolved', label: 'Resolvido', icon: 'pi pi-check', color: 'emerald' }
|
|
];
|
|
|
|
const CHANNEL_OPTIONS = [
|
|
{ label: 'Todos', value: null },
|
|
{ label: 'WhatsApp', value: 'whatsapp' },
|
|
{ label: 'SMS', value: 'sms' },
|
|
{ label: 'E-mail', value: 'email' }
|
|
];
|
|
|
|
// Helpers
|
|
function fmtRelative(iso) {
|
|
if (!iso) return '';
|
|
const d = new Date(iso);
|
|
const now = new Date();
|
|
const diff = Math.floor((now - d) / 1000);
|
|
if (diff < 60) return 'agora';
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
|
|
if (diff < 604800) return `${Math.floor(diff / 86400)}d`;
|
|
return d.toLocaleDateString('pt-BR');
|
|
}
|
|
function channelIcon(ch) {
|
|
const map = { whatsapp: 'pi-whatsapp', sms: 'pi-comment', email: 'pi-envelope' };
|
|
return map[ch] || 'pi-comment';
|
|
}
|
|
function truncate(s, n = 80) {
|
|
if (!s) return '';
|
|
const str = String(s).replace(/\s+/g, ' ').trim();
|
|
return str.length > n ? str.slice(0, n - 1) + '…' : str;
|
|
}
|
|
function contactLabel(thread) {
|
|
return thread.patient_name || thread.contact_number || 'Desconhecido';
|
|
}
|
|
|
|
function onCardClick(thread) {
|
|
drawerStore.openForThread(thread);
|
|
if (isMobile.value) fecharDrawer();
|
|
}
|
|
|
|
const carregandoInicial = computed(
|
|
() => loading.value && filteredThreads.value.length === 0
|
|
);
|
|
|
|
const unlinkedCount = computed(() => filteredThreads.value.filter((t) => !t.patient_id).length);
|
|
|
|
// ── "Limpar filtros" global (footer fixo da sidebar) ─────────────
|
|
// `filters` é um ref({...}) (vide useConversations.js). No script
|
|
// preciso acessar via .value; no template o auto-unwrap cuida.
|
|
const hasActiveFilters = computed(() =>
|
|
!!(filters.value.search || filters.value.unreadOnly || filters.value.assigned || filters.value.channel)
|
|
);
|
|
function clearAllFilters() {
|
|
filters.value.search = '';
|
|
filters.value.unreadOnly = false;
|
|
filters.value.assigned = null;
|
|
filters.value.channel = null;
|
|
}
|
|
|
|
// Popover de Ações (compact)
|
|
const actionsPopRef = ref(null);
|
|
function openActions(e) { actionsPopRef.value?.toggle(e); }
|
|
|
|
// Lifecycle
|
|
onMounted(async () => {
|
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
|
isMobile.value = _mqMobile.matches;
|
|
_mqMobile.addEventListener('change', _onMqMobileChange);
|
|
_mqCompact = window.matchMedia('(max-width: 1279px)');
|
|
isCompact.value = _mqCompact.matches;
|
|
_mqCompact.addEventListener('change', _onMqCompactChange);
|
|
}
|
|
await load();
|
|
subscribeRealtime();
|
|
await Promise.all([tagsApi.loadAllTags(), reloadThreadTags(), loadMemberNames()]);
|
|
});
|
|
onBeforeUnmount(() => {
|
|
if (_mqMobile) _mqMobile.removeEventListener('change', _onMqMobileChange);
|
|
if (_mqCompact) _mqCompact.removeEventListener('change', _onMqCompactChange);
|
|
unsubscribeRealtime();
|
|
});
|
|
|
|
// Recarrega tags quando drawer fecha
|
|
watch(() => drawerStore.isOpen, (isOpen) => {
|
|
if (!isOpen) { reloadThreadTags(); load(); }
|
|
});
|
|
watch(() => filteredThreads.value.length, () => { reloadThreadTags(); });
|
|
watch(() => tenantStore.activeTenantId, async () => {
|
|
unsubscribeRealtime();
|
|
await load();
|
|
subscribeRealtime();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Drawer host (blueprint §2) -->
|
|
<aside
|
|
class="mw-mobile-drawer"
|
|
:class="{ 'is-open': drawerOpen }"
|
|
v-show="isMobile"
|
|
aria-label="Filtros & atribuição"
|
|
>
|
|
<div id="mw-mobile-drawer-target" class="mw-mobile-drawer__scroll" />
|
|
</aside>
|
|
<Transition name="mw-drawer-fade">
|
|
<div
|
|
v-if="isMobile && drawerOpen"
|
|
class="mw-mobile-drawer__backdrop"
|
|
@click="fecharDrawer"
|
|
/>
|
|
</Transition>
|
|
|
|
<section class="mw-page">
|
|
<header class="mw-page__head">
|
|
<button
|
|
class="mw-menu-btn mw-menu-btn--mobile-only"
|
|
v-tooltip.bottom="'Filtros & atribuição'"
|
|
@click="toggleDrawer"
|
|
>
|
|
<i class="pi pi-bars" />
|
|
<span>Menu Conversas</span>
|
|
</button>
|
|
<div class="mw-page__title">
|
|
<i class="pi pi-comments text-emerald-300" />
|
|
<span>Conversas</span>
|
|
<span class="mw-page__count">{{ summary.total }}</span>
|
|
<span v-if="summary.unreadTotal > 0" class="mw-page__unread">
|
|
{{ summary.unreadTotal }} não lida{{ summary.unreadTotal !== 1 ? 's' : '' }}
|
|
</span>
|
|
</div>
|
|
<div class="mw-page__actions">
|
|
<div class="mw-search mw-search--xl-only">
|
|
<i class="pi pi-search mw-search__icon" />
|
|
<input
|
|
v-model="filters.search"
|
|
type="text"
|
|
placeholder="Buscar paciente, número ou mensagem"
|
|
maxlength="120"
|
|
class="mw-search__input"
|
|
/>
|
|
</div>
|
|
<button
|
|
class="mw-head-btn mw-head-btn--compact-only"
|
|
v-tooltip.bottom="'Buscar'"
|
|
@click="openActions"
|
|
>
|
|
<i class="pi pi-search" />
|
|
</button>
|
|
<button
|
|
class="mw-head-btn"
|
|
v-tooltip.bottom="'Recarregar'"
|
|
:disabled="loading"
|
|
@click="load"
|
|
>
|
|
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
|
|
</button>
|
|
<button class="mw-close" v-tooltip.bottom="'Voltar ao resumo (Esc)'" @click="emit('close')">
|
|
<i class="pi pi-times" />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<Popover ref="actionsPopRef" class="mw-actions-pop">
|
|
<div class="mw-actions">
|
|
<div class="mw-actions__group">
|
|
<div class="mw-actions__label">Buscar</div>
|
|
<InputText
|
|
v-model="filters.search"
|
|
placeholder="Paciente, número ou mensagem…"
|
|
maxlength="120"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Popover>
|
|
|
|
<!-- Subheader explicativo (blueprint §9) — diferencia de
|
|
outras páginas Melissa que mostram listas tabulares. -->
|
|
<div class="mw-subheader">
|
|
<i class="pi pi-info-circle mw-subheader__icon" />
|
|
<span class="mw-subheader__text">
|
|
CRM de mensagens organizado em <strong>kanban por urgência</strong>:
|
|
Urgente / Aguardando resposta / Aguardando paciente / Resolvido.
|
|
Click num card abre a conversa no painel lateral.
|
|
</span>
|
|
</div>
|
|
|
|
<div class="mw-body">
|
|
<!-- ═══ COL 1: Filtros + atribuição + canais + status ═══ -->
|
|
<Teleport to="#mw-mobile-drawer-target" :disabled="!isMobile">
|
|
<aside class="mw-side">
|
|
<div class="mw-side__scroll">
|
|
<!-- Alerta unlinked — no topo pra ficar bem visível
|
|
(números de telefone sem paciente vinculado). -->
|
|
<div v-if="unlinkedCount > 0" class="mw-alert">
|
|
<i class="pi pi-exclamation-circle" />
|
|
<div>
|
|
<div class="mw-alert__title">{{ unlinkedCount }} sem paciente vinculado</div>
|
|
<div class="mw-alert__hint">Números de telefone que não batem com pacientes cadastrados.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filtros rápidos -->
|
|
<div class="mw-w mw-w--side">
|
|
<div class="mw-w__head">
|
|
<span class="mw-w__title"><i class="pi pi-filter" /> Filtros rápidos</span>
|
|
<button
|
|
v-if="filters.unreadOnly"
|
|
class="mw-side__clear-inline"
|
|
v-tooltip.top="'Limpar filtro de não lidas'"
|
|
aria-label="Limpar filtro de não lidas"
|
|
@click="filters.unreadOnly = false"
|
|
>
|
|
<i class="pi pi-times" />
|
|
</button>
|
|
</div>
|
|
<div class="mw-side__list">
|
|
<button
|
|
class="mw-side__item"
|
|
:class="{ 'is-active': !filters.unreadOnly }"
|
|
@click="filters.unreadOnly = false"
|
|
>
|
|
<i class="pi pi-list" />
|
|
<span>Todas</span>
|
|
<span class="mw-side__count">{{ summary.total }}</span>
|
|
</button>
|
|
<button
|
|
class="mw-side__item"
|
|
:class="{ 'is-active': filters.unreadOnly, 'is-warn': summary.unreadTotal > 0 }"
|
|
@click="filters.unreadOnly = !filters.unreadOnly"
|
|
>
|
|
<i class="pi pi-bell" />
|
|
<span>Não lidas</span>
|
|
<span class="mw-side__count" :class="{ 'is-danger': summary.unreadTotal > 0 }">{{ summary.unreadTotal }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Atribuição -->
|
|
<div class="mw-w mw-w--side">
|
|
<div class="mw-w__head">
|
|
<span class="mw-w__title"><i class="pi pi-user" /> Atribuição</span>
|
|
<button
|
|
v-if="filters.assigned"
|
|
class="mw-side__clear-inline"
|
|
v-tooltip.top="'Limpar filtro de atribuição'"
|
|
aria-label="Limpar filtro de atribuição"
|
|
@click="filters.assigned = null"
|
|
>
|
|
<i class="pi pi-times" />
|
|
</button>
|
|
</div>
|
|
<div class="mw-side__list">
|
|
<button
|
|
class="mw-side__item"
|
|
:class="{ 'is-active': !filters.assigned }"
|
|
@click="filters.assigned = null"
|
|
>
|
|
<i class="pi pi-list" />
|
|
<span>Todas</span>
|
|
</button>
|
|
<button
|
|
class="mw-side__item"
|
|
:class="{ 'is-active': filters.assigned === 'me' }"
|
|
@click="filters.assigned = 'me'"
|
|
>
|
|
<i class="pi pi-user" />
|
|
<span>Minhas</span>
|
|
<span class="mw-side__count">{{ mineCount }}</span>
|
|
</button>
|
|
<button
|
|
class="mw-side__item"
|
|
:class="{ 'is-active': filters.assigned === 'unassigned' }"
|
|
@click="filters.assigned = 'unassigned'"
|
|
>
|
|
<i class="pi pi-user-minus" />
|
|
<span>Não atribuídas</span>
|
|
<span class="mw-side__count" :class="{ 'is-warn': unassignedCount > 0 }">{{ unassignedCount }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Por status (kanban resumo — display-only, sem X) -->
|
|
<div class="mw-w mw-w--side">
|
|
<div class="mw-w__head">
|
|
<span class="mw-w__title"><i class="pi pi-chart-bar" /> Por status</span>
|
|
</div>
|
|
<div class="mw-side__list">
|
|
<div
|
|
v-for="col in KANBAN_COLUMNS"
|
|
:key="col.key"
|
|
class="mw-side__row"
|
|
:class="`is-${col.color}`"
|
|
>
|
|
<i :class="col.icon" />
|
|
<span>{{ col.label }}</span>
|
|
<span class="mw-side__count">{{ summary[col.key] || 0 }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Canais -->
|
|
<div class="mw-w mw-w--side">
|
|
<div class="mw-w__head">
|
|
<span class="mw-w__title"><i class="pi pi-send" /> Canais</span>
|
|
<button
|
|
v-if="filters.channel"
|
|
class="mw-side__clear-inline"
|
|
v-tooltip.top="'Limpar filtro de canal'"
|
|
aria-label="Limpar filtro de canal"
|
|
@click="filters.channel = null"
|
|
>
|
|
<i class="pi pi-times" />
|
|
</button>
|
|
</div>
|
|
<div class="mw-side__list">
|
|
<button
|
|
v-for="opt in CHANNEL_OPTIONS"
|
|
:key="String(opt.value)"
|
|
class="mw-side__item"
|
|
:class="{ 'is-active': filters.channel === opt.value }"
|
|
@click="filters.channel = opt.value"
|
|
>
|
|
<i v-if="opt.value" :class="['pi', channelIcon(opt.value)]" />
|
|
<i v-else class="pi pi-list" />
|
|
<span>{{ opt.label }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Footer fixo: "Limpar filtros" global (zera busca,
|
|
unread, atribuição e canal de uma vez). -->
|
|
<Transition name="mw-clear">
|
|
<div v-if="hasActiveFilters" class="mw-side__footer">
|
|
<button class="mw-side__clear-all" @click="clearAllFilters">
|
|
<i class="pi pi-filter-slash" />
|
|
<span>Limpar filtros</span>
|
|
</button>
|
|
</div>
|
|
</Transition>
|
|
</aside>
|
|
</Teleport>
|
|
|
|
<!-- ═══ COL 2: Kanban grid ═══ -->
|
|
<div class="mw-main">
|
|
<!-- Kanban -->
|
|
<div class="mw-kanban">
|
|
<div
|
|
v-for="col in KANBAN_COLUMNS"
|
|
:key="col.key"
|
|
class="mw-col"
|
|
:class="`is-${col.color}`"
|
|
>
|
|
<div class="mw-col__head">
|
|
<div class="mw-col__title">
|
|
<i :class="col.icon" />
|
|
<span>{{ col.label }}</span>
|
|
</div>
|
|
<span class="mw-col__count">{{ byKanban[col.key].length }}</span>
|
|
</div>
|
|
<div class="mw-col__body">
|
|
<!-- Skeleton só na primeira carga, na coluna que estiver vazia -->
|
|
<template v-if="carregandoInicial && !byKanban[col.key].length">
|
|
<div v-for="i in 2" :key="`csk-${col.key}-${i}`" class="mw-card mw-card--skeleton" aria-busy="true">
|
|
<div style="display:flex; flex-direction:column; gap:6px; flex:1;">
|
|
<span class="melissa-skeleton melissa-skeleton--title" :style="{ width: `${50 + (i * 13) % 30}%` }" />
|
|
<span class="melissa-skeleton melissa-skeleton--text" style="width: 80%;" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-else-if="!byKanban[col.key].length" class="mw-col__empty">
|
|
<i class="pi pi-comments" />
|
|
<span>Nenhuma conversa</span>
|
|
</div>
|
|
|
|
<button
|
|
v-for="t in byKanban[col.key]"
|
|
v-else
|
|
:key="t.thread_key"
|
|
class="mw-card"
|
|
@click="onCardClick(t)"
|
|
>
|
|
<div class="mw-card__head">
|
|
<i :class="['pi', channelIcon(t.channel), 'mw-card__channel']" />
|
|
<span class="mw-card__name">{{ contactLabel(t) }}</span>
|
|
<span v-if="t.unread_count > 0" class="mw-card__unread">{{ t.unread_count }}</span>
|
|
</div>
|
|
<div class="mw-card__msg">
|
|
<i v-if="t.last_message_direction === 'outbound'" class="pi pi-arrow-right text-[0.55rem] mr-1 opacity-60" />
|
|
{{ truncate(t.last_message_body, 70) }}
|
|
</div>
|
|
<div v-if="tagsForThread(t.thread_key).length" class="mw-card__tags">
|
|
<span
|
|
v-for="tag in tagsForThread(t.thread_key)"
|
|
:key="tag.id"
|
|
class="mw-card__tag"
|
|
:style="{ background: tag.color + '20', color: tag.color, borderColor: tag.color + '40' }"
|
|
>
|
|
<i v-if="tag.icon" :class="tag.icon" />
|
|
{{ tag.name }}
|
|
</span>
|
|
</div>
|
|
<div class="mw-card__foot">
|
|
<span class="mw-card__time">{{ fmtRelative(t.last_message_at) }}</span>
|
|
<span
|
|
v-if="t.assigned_to"
|
|
class="mw-card__assignee"
|
|
:class="{ 'is-mine': t.assigned_to === currentUserId }"
|
|
v-tooltip.top="t.assigned_to === currentUserId ? 'Atribuída a mim' : 'Atribuída a ' + (memberNameMap[t.assigned_to] || '')"
|
|
>
|
|
<i class="pi pi-user" />
|
|
{{ t.assigned_to === currentUserId ? 'Eu' : assigneeLabel(t.assigned_to) }}
|
|
</span>
|
|
<span v-else-if="!t.patient_name" class="mw-card__unlinked">não vinculado</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Convenção Melissa Page (blueprint §6) */
|
|
.mw-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: mw-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
|
}
|
|
@keyframes mw-page-enter {
|
|
from { opacity: 0; transform: scale(0.985); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
|
|
.mw-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;
|
|
}
|
|
.mw-page__title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
flex-wrap: wrap;
|
|
}
|
|
.mw-page__title > span:not(.mw-page__count):not(.mw-page__unread) {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.mw-page__count {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
color: var(--m-accent);
|
|
background: var(--m-accent-soft);
|
|
border: 1px solid color-mix(in srgb, var(--m-accent) 35%, transparent);
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
}
|
|
.mw-page__unread {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
color: rgb(248, 113, 113);
|
|
background: rgba(248, 113, 113, 0.12);
|
|
border: 1px solid rgba(248, 113, 113, 0.3);
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
}
|
|
.mw-page__actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
.mw-close,
|
|
.mw-head-btn {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: grid;
|
|
place-items: center;
|
|
background: var(--m-bg-soft);
|
|
border: 1px solid var(--m-border);
|
|
color: var(--m-text);
|
|
border-radius: 9px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
transition: background-color 140ms ease;
|
|
}
|
|
.mw-close:hover,
|
|
.mw-head-btn:hover { background: var(--m-bg-soft-hover); }
|
|
.mw-head-btn > i { font-size: 0.85rem; }
|
|
.mw-head-btn--compact-only { display: none; }
|
|
|
|
/* Search inline (xl+) */
|
|
.mw-search {
|
|
position: relative;
|
|
width: 240px;
|
|
}
|
|
.mw-search__icon {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 12px;
|
|
transform: translateY(-50%);
|
|
color: var(--m-text-muted);
|
|
font-size: 0.78rem;
|
|
}
|
|
.mw-search__input {
|
|
width: 100%;
|
|
height: 32px;
|
|
background: var(--m-bg-soft);
|
|
border: 1px solid var(--m-border);
|
|
color: var(--m-text);
|
|
padding: 0 12px 0 32px;
|
|
border-radius: 9px;
|
|
font-size: 0.78rem;
|
|
font-family: inherit;
|
|
outline: none;
|
|
transition: border-color 140ms ease, background-color 140ms ease;
|
|
}
|
|
.mw-search__input:focus {
|
|
background: var(--m-bg-soft-hover);
|
|
border-color: var(--m-border-strong);
|
|
}
|
|
.mw-search__input::placeholder { color: var(--m-text-faint); }
|
|
|
|
/* Menu mobile */
|
|
.mw-menu-btn {
|
|
display: none;
|
|
height: 32px;
|
|
align-items: center;
|
|
gap: 6px;
|
|
flex-shrink: 0;
|
|
background: var(--m-accent);
|
|
border: 1px solid var(--m-accent);
|
|
color: white;
|
|
padding: 0 11px;
|
|
border-radius: 9px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
transition: background-color 140ms ease, transform 140ms ease;
|
|
}
|
|
.mw-menu-btn:hover {
|
|
background: color-mix(in srgb, var(--m-accent) 88%, white);
|
|
transform: translateY(-1px);
|
|
}
|
|
.mw-menu-btn > i { font-size: 0.85rem; }
|
|
|
|
/* Subheader explicativo (blueprint §9) */
|
|
.mw-subheader {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 10px 18px;
|
|
border-bottom: 1px solid var(--m-border);
|
|
background: var(--m-bg-soft);
|
|
font-size: 0.78rem;
|
|
color: var(--m-text-muted);
|
|
line-height: 1.45;
|
|
flex-shrink: 0;
|
|
}
|
|
.mw-subheader__icon {
|
|
color: var(--p-primary-color);
|
|
font-size: 0.92rem;
|
|
flex-shrink: 0;
|
|
margin-top: 1px;
|
|
}
|
|
.mw-subheader__text { flex: 1; min-width: 0; }
|
|
.mw-subheader__text strong { color: var(--m-text); font-weight: 600; }
|
|
|
|
/* Body — sem padding/gap; a sidebar tem bg+border-right próprios e o
|
|
main column controla seu padding interno. Espelha o pattern usado
|
|
em MelissaGrupos / MelissaTags / MelissaMedicos. */
|
|
.mw-body {
|
|
flex: 1;
|
|
display: flex;
|
|
min-height: 0;
|
|
position: relative;
|
|
gap: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Aside — 2 zonas: __scroll (cards) + __footer (Limpar filtros fixo).
|
|
bg colorido próprio (--m-bg-soft) + border-right pra separar
|
|
visualmente da coluna principal. */
|
|
.mw-side {
|
|
width: 280px;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--m-bg-soft);
|
|
border-right: 1px solid var(--m-border);
|
|
overflow: hidden;
|
|
}
|
|
.mw-side__scroll {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--m-border-strong) transparent;
|
|
}
|
|
.mw-side__scroll::-webkit-scrollbar { width: 5px; }
|
|
.mw-side__scroll::-webkit-scrollbar-thumb {
|
|
background: var(--m-border-strong);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* Footer fixo no bottom da sidebar (fora do scroll dos filter cards).
|
|
Aparece com fade+collapse quando algum filtro está ativo. */
|
|
.mw-side__footer {
|
|
flex-shrink: 0;
|
|
padding: 12px;
|
|
background: var(--m-bg-soft);
|
|
border-top: 1px solid var(--m-border);
|
|
}
|
|
.mw-side__clear-all {
|
|
width: 100%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 9px 12px;
|
|
background: var(--m-bg-medium);
|
|
border: 1px solid var(--m-border);
|
|
color: var(--m-text);
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
|
|
}
|
|
.mw-side__clear-all:hover {
|
|
background: var(--m-bg-soft-hover);
|
|
border-color: var(--m-border-strong);
|
|
color: var(--m-text);
|
|
}
|
|
.mw-side__clear-all > i {
|
|
font-size: 0.78rem;
|
|
color: var(--m-text-muted);
|
|
transition: color 140ms ease;
|
|
}
|
|
.mw-side__clear-all:hover > i { color: var(--m-text); }
|
|
|
|
/* X inline ao lado do título de cada filter card — limpa o filtro
|
|
individual. Espelha o pattern do MelissaPacientes/Grupos/Tags. */
|
|
.mw-side__clear-inline {
|
|
width: 18px;
|
|
height: 18px;
|
|
display: grid;
|
|
place-items: center;
|
|
background: transparent;
|
|
border: 1px solid color-mix(in srgb, rgb(220, 38, 38) 30%, var(--m-border));
|
|
color: rgb(220, 38, 38);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
transition: background-color 140ms ease, border-color 140ms ease;
|
|
}
|
|
.mw-side__clear-inline:hover {
|
|
background: rgba(220, 38, 38, 0.10);
|
|
border-color: rgba(220, 38, 38, 0.55);
|
|
}
|
|
.mw-side__clear-inline > i { font-size: 0.6rem; }
|
|
|
|
/* Transition do footer "Limpar filtros" */
|
|
.mw-clear-enter-active,
|
|
.mw-clear-leave-active {
|
|
transition: opacity 220ms ease, transform 220ms ease, max-height 240ms ease;
|
|
overflow: hidden;
|
|
}
|
|
.mw-clear-enter-from,
|
|
.mw-clear-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(6px);
|
|
max-height: 0;
|
|
}
|
|
.mw-clear-enter-to,
|
|
.mw-clear-leave-from {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
max-height: 80px;
|
|
}
|
|
|
|
/* Card-base — alinhado com .ma-w / .mp-w / .mcr-w: surface --m-bg-medium
|
|
pra destacar do bg da sidebar (--m-bg-soft). */
|
|
.mw-w {
|
|
background: var(--m-bg-medium);
|
|
border: 1px solid var(--m-border);
|
|
border-radius: 12px;
|
|
padding: 12px;
|
|
}
|
|
/* Modifier pros cards dentro da .mw-side — margem lateral + sombra
|
|
sutil pra elevar sobre o bg da sidebar. Espelha .mc-w--side, .mt-w--side. */
|
|
.mw-w--side {
|
|
margin: 12px 12px 0;
|
|
flex-shrink: 0;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
}
|
|
.mw-w--side:last-of-type { margin-bottom: 12px; }
|
|
.mw-w__head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 10px;
|
|
}
|
|
.mw-w__title {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
}
|
|
.mw-w__title > i { color: var(--m-text-muted); font-size: 0.78rem; }
|
|
|
|
.mw-side__list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.mw-side__item {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 10px;
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
color: var(--m-text);
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
font-size: 0.82rem;
|
|
text-align: left;
|
|
transition: background-color 140ms ease, border-color 140ms ease;
|
|
}
|
|
.mw-side__item:hover { background: var(--m-bg-soft-hover); }
|
|
.mw-side__item.is-active {
|
|
background: var(--m-accent-soft);
|
|
border-color: color-mix(in srgb, var(--m-accent) 35%, var(--m-border));
|
|
}
|
|
.mw-side__item.is-warn { background: rgba(248, 113, 113, 0.05); }
|
|
.mw-side__item > i {
|
|
color: var(--m-text-muted);
|
|
font-size: 0.75rem;
|
|
width: 14px;
|
|
text-align: center;
|
|
}
|
|
.mw-side__item > span:first-of-type { flex: 1; }
|
|
|
|
.mw-side__count {
|
|
font-size: 0.65rem;
|
|
font-weight: 600;
|
|
color: var(--m-text-muted);
|
|
background: var(--m-bg-medium);
|
|
padding: 1px 7px;
|
|
border-radius: 999px;
|
|
min-width: 22px;
|
|
text-align: center;
|
|
}
|
|
.mw-side__count.is-danger {
|
|
background: rgba(248, 113, 113, 0.15);
|
|
color: rgb(248, 113, 113);
|
|
}
|
|
.mw-side__count.is-warn {
|
|
background: rgba(251, 191, 36, 0.15);
|
|
color: rgb(251, 191, 36);
|
|
}
|
|
|
|
.mw-side__row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 6px 10px;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--m-border);
|
|
background: var(--m-bg-medium);
|
|
font-size: 0.78rem;
|
|
}
|
|
.mw-side__row > span:first-of-type { flex: 1; }
|
|
.mw-side__row > i { font-size: 0.75rem; }
|
|
.mw-side__row.is-red { border-color: rgba(248, 113, 113, 0.3); background: rgba(248, 113, 113, 0.05); }
|
|
.mw-side__row.is-red > i { color: rgb(248, 113, 113); }
|
|
.mw-side__row.is-amber { border-color: rgba(251, 191, 36, 0.3); background: rgba(251, 191, 36, 0.05); }
|
|
.mw-side__row.is-amber > i { color: rgb(251, 191, 36); }
|
|
.mw-side__row.is-blue { border-color: rgba(96, 165, 250, 0.3); background: rgba(96, 165, 250, 0.05); }
|
|
.mw-side__row.is-blue > i { color: rgb(96, 165, 250); }
|
|
.mw-side__row.is-emerald { border-color: rgba(74, 222, 128, 0.3); background: rgba(74, 222, 128, 0.05); }
|
|
.mw-side__row.is-emerald > i { color: rgb(74, 222, 128); }
|
|
|
|
.mw-alert {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin: 12px 12px 0;
|
|
padding: 12px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(251, 191, 36, 0.3);
|
|
background: rgba(251, 191, 36, 0.05);
|
|
color: rgb(251, 191, 36);
|
|
flex-shrink: 0;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
}
|
|
.mw-alert:last-child { margin-bottom: 12px; }
|
|
.mw-alert > i { font-size: 0.85rem; margin-top: 2px; }
|
|
.mw-alert__title {
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
}
|
|
.mw-alert__hint {
|
|
font-size: 0.7rem;
|
|
color: var(--m-text-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* Main — recebe padding interno (o body não tem mais padding/gap;
|
|
sidebar fica colada à esquerda com border-right). */
|
|
.mw-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 12px;
|
|
}
|
|
|
|
/* Kanban */
|
|
.mw-kanban {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 12px;
|
|
min-height: 0;
|
|
}
|
|
.mw-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* Coluna do kanban também é "card" — mesma surface dos demais. */
|
|
background: var(--m-bg-medium);
|
|
border: 1px solid var(--m-border);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
min-height: 0;
|
|
}
|
|
.mw-col.is-red { border-color: rgba(248, 113, 113, 0.35); }
|
|
.mw-col.is-amber { border-color: rgba(251, 191, 36, 0.35); }
|
|
.mw-col.is-blue { border-color: rgba(96, 165, 250, 0.35); }
|
|
.mw-col.is-emerald { border-color: rgba(74, 222, 128, 0.35); }
|
|
|
|
.mw-col__head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid var(--m-border);
|
|
flex-shrink: 0;
|
|
}
|
|
.mw-col.is-red .mw-col__head { background: rgba(248, 113, 113, 0.05); border-bottom-color: rgba(248, 113, 113, 0.25); }
|
|
.mw-col.is-amber .mw-col__head { background: rgba(251, 191, 36, 0.05); border-bottom-color: rgba(251, 191, 36, 0.25); }
|
|
.mw-col.is-blue .mw-col__head { background: rgba(96, 165, 250, 0.05); border-bottom-color: rgba(96, 165, 250, 0.25); }
|
|
.mw-col.is-emerald .mw-col__head { background: rgba(74, 222, 128, 0.05); border-bottom-color: rgba(74, 222, 128, 0.25); }
|
|
|
|
.mw-col__title {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
}
|
|
.mw-col__title > i { font-size: 0.78rem; }
|
|
.mw-col.is-red .mw-col__title > i { color: rgb(248, 113, 113); }
|
|
.mw-col.is-amber .mw-col__title > i { color: rgb(251, 191, 36); }
|
|
.mw-col.is-blue .mw-col__title > i { color: rgb(96, 165, 250); }
|
|
.mw-col.is-emerald .mw-col__title > i { color: rgb(74, 222, 128); }
|
|
|
|
.mw-col__count {
|
|
font-size: 0.65rem;
|
|
font-weight: 600;
|
|
color: var(--m-text);
|
|
background: var(--m-bg-medium);
|
|
padding: 1px 7px;
|
|
border-radius: 999px;
|
|
min-width: 22px;
|
|
text-align: center;
|
|
}
|
|
.mw-col__body {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--m-border-strong) transparent;
|
|
}
|
|
.mw-col__body::-webkit-scrollbar { width: 5px; }
|
|
.mw-col__body::-webkit-scrollbar-thumb {
|
|
background: var(--m-border-strong);
|
|
border-radius: 3px;
|
|
}
|
|
.mw-col__empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 24px 12px;
|
|
color: var(--m-text-faint);
|
|
font-size: 0.72rem;
|
|
font-style: italic;
|
|
}
|
|
.mw-col__empty > i { font-size: 1.5rem; opacity: 0.4; }
|
|
|
|
/* Card de conversa — fica DENTRO da coluna .mw-col (que agora usa
|
|
--m-bg-medium). Pra ter contraste, o card usa --m-bg-soft (mais sutil). */
|
|
.mw-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
padding: 10px 12px;
|
|
background: var(--m-bg-soft);
|
|
border: 1px solid var(--m-border);
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
font-family: inherit;
|
|
color: var(--m-text);
|
|
transition: background-color 140ms ease, border-color 140ms ease, transform 140ms ease;
|
|
}
|
|
.mw-card:hover {
|
|
background: var(--m-bg-soft-hover);
|
|
border-color: color-mix(in srgb, var(--m-accent) 40%, var(--m-border));
|
|
transform: translateY(-1px);
|
|
}
|
|
.mw-card--skeleton { cursor: default; pointer-events: none; opacity: 0.95; }
|
|
.mw-card--skeleton:hover { background: var(--m-bg-soft); transform: none; }
|
|
|
|
.mw-card__head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.mw-card__channel {
|
|
color: var(--m-text-muted);
|
|
font-size: 0.7rem;
|
|
}
|
|
.mw-card__name {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.mw-card__unread {
|
|
font-size: 0.62rem;
|
|
font-weight: 700;
|
|
color: white;
|
|
background: rgb(248, 113, 113);
|
|
padding: 1px 6px;
|
|
border-radius: 999px;
|
|
min-width: 18px;
|
|
text-align: center;
|
|
}
|
|
.mw-card__msg {
|
|
font-size: 0.74rem;
|
|
color: var(--m-text-muted);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.mw-card__tags {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.mw-card__tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
font-size: 0.6rem;
|
|
font-weight: 600;
|
|
padding: 1px 7px;
|
|
border-radius: 999px;
|
|
border: 1px solid;
|
|
line-height: 1.4;
|
|
}
|
|
.mw-card__tag > i { font-size: 0.55rem; }
|
|
|
|
.mw-card__foot {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 6px;
|
|
font-size: 0.65rem;
|
|
color: var(--m-text-faint);
|
|
}
|
|
.mw-card__time { font-weight: 500; }
|
|
.mw-card__assignee {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
padding: 1px 6px;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--m-border);
|
|
background: var(--m-bg-soft);
|
|
color: var(--m-text);
|
|
font-size: 0.6rem;
|
|
font-weight: 600;
|
|
}
|
|
.mw-card__assignee.is-mine {
|
|
background: var(--m-accent-soft);
|
|
border-color: color-mix(in srgb, var(--m-accent) 35%, var(--m-border));
|
|
color: var(--m-accent);
|
|
}
|
|
.mw-card__assignee > i { font-size: 0.55rem; }
|
|
.mw-card__unlinked { font-style: italic; }
|
|
|
|
/* Popover */
|
|
.mw-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
min-width: 240px;
|
|
padding: 4px;
|
|
}
|
|
.mw-actions__group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.mw-actions__label {
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.14em;
|
|
color: var(--text-color-secondary, var(--m-text-faint));
|
|
font-size: 0.62rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Drawer mobile (blueprint §6) */
|
|
.mw-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;
|
|
}
|
|
.mw-mobile-drawer.is-open { transform: translateX(0); }
|
|
.mw-mobile-drawer__scroll {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.mw-mobile-drawer__scroll .mw-side {
|
|
flex: 1;
|
|
min-height: 0;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
background: transparent;
|
|
border-right: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.mw-mobile-drawer__scroll .mw-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;
|
|
}
|
|
.mw-mobile-drawer__scroll .mw-side__scroll::-webkit-scrollbar { width: 5px; }
|
|
.mw-mobile-drawer__scroll .mw-side__scroll::-webkit-scrollbar-thumb {
|
|
background: var(--m-border-strong);
|
|
border-radius: 3px;
|
|
}
|
|
.mw-mobile-drawer__scroll .mw-w--side {
|
|
margin: 0;
|
|
flex-shrink: 0;
|
|
}
|
|
.mw-mobile-drawer__scroll .mw-w--side:last-of-type { margin-bottom: 0; }
|
|
.mw-mobile-drawer__scroll .mw-alert { margin: 0; flex-shrink: 0; }
|
|
.mw-mobile-drawer__scroll .mw-side__footer {
|
|
flex-shrink: 0;
|
|
margin: 0;
|
|
padding: 12px;
|
|
background: var(--m-bg-medium);
|
|
border-top: 1px solid var(--m-border);
|
|
backdrop-filter: blur(24px) saturate(160%);
|
|
-webkit-backdrop-filter: blur(24px) saturate(160%);
|
|
}
|
|
.mw-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;
|
|
}
|
|
.mw-drawer-fade-enter-active,
|
|
.mw-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
|
.mw-drawer-fade-enter-from,
|
|
.mw-drawer-fade-leave-to { opacity: 0; }
|
|
|
|
/* ═══ Compact (<xl) — busca migra pra popover, kanban 2-col ═══ */
|
|
@media (max-width: 1279px) {
|
|
.mw-search--xl-only { display: none; }
|
|
.mw-head-btn--compact-only { display: grid; }
|
|
.mw-kanban { grid-template-columns: repeat(2, 1fr); }
|
|
}
|
|
|
|
/* ═══ Mobile (<lg) — drawer + kanban 1-col stacked ═══
|
|
Em mobile o kanban vira flex column (stacked) e o scroll passa a ser
|
|
global no .mw-main (não interno por coluna). Cada .mw-col cresce com
|
|
o conteúdo + min-height pra empty state ter altura visível. */
|
|
@media (max-width: 1023px) {
|
|
.mw-body { flex-direction: column; padding: 0; }
|
|
.mw-main {
|
|
width: 100%;
|
|
padding: 8px;
|
|
overflow-y: auto;
|
|
}
|
|
.mw-kanban {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: none;
|
|
gap: 8px;
|
|
}
|
|
.mw-col {
|
|
flex: none;
|
|
min-height: 200px;
|
|
}
|
|
.mw-col__body {
|
|
flex: none;
|
|
overflow: visible;
|
|
min-height: 80px;
|
|
}
|
|
.mw-page__title > span:first-of-type { display: none; }
|
|
.mw-menu-btn--mobile-only { display: inline-flex; }
|
|
}
|
|
</style>
|