Files
agenciapsilmno/src/layout/melissa/MelissaGrupos.vue
T
Leonardo bad828cab3 Melissa pages: esconde page-title-icon em mobile
No mobile o botao "Menu Lancamentos/Notificacoes/etc" ja indica a sessao,
entao o pi-list/pi-bell ao lado do contador era redundante. Adiciona
.<prefix>-page__title-icon { display: none; } no @media max-width: 1023px.
Em MelissaConversas usa > i:first-child (icone nao tem classe dedicada).

Pages: FinanceiroLancamentos, Compromissos, Documentos, CadastrosRecebidos,
Conversas, AgendamentosRecebidos, Financeiro, Grupos, Notificacoes, Tags,
Medicos, Relatorios, Recorrencias.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:54:28 -03:00

2410 lines
82 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>
/*
* MelissaGrupos — Grupos de pacientes (CRUD).
* Segue blueprint melissa-table-page-blueprint.md (DataTable + sidebar com
* filtros coloridos, view toggle list/grade, subheader explicativo, footer
* fixo com "Limpar filtros", Xs inline pra zerar filtro individual).
*
* Tabela: patient_groups; vínculo: patient_group_patient. Sem view agregada
* — contagem feita no client após carregar vínculos.
*
* Row design preservado: dot colorido grande + nome + badge "Sistema" +
* contagem de pacientes na lista; coluna "Ações" frozen 100px com pencil
* e trash (ocultos em is_system).
*/
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
// DataTable/Column/Paginator/Dialog/InputText/Button: auto via PrimeVueResolver
const emit = defineEmits(['close']);
const toast = useToast();
const confirm = useConfirm();
const tenantStore = useTenantStore();
// ── 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; }
// ── Estado ─────────────────────────────────────────────────
const loading = ref(false);
const saving = ref(false);
const grupos = ref([]);
const counts = ref(new Map()); // groupId → patient count
const busca = ref('');
const tipoFilter = ref(''); // '' | 'system' | 'mine'
const usoFilter = ref(''); // '' | 'em_uso' | 'vazio'
// ── Helpers de status (rowClass) ──────────────────────────
function rowStatusClass(g) {
if (!g) return '';
if (g.is_system) return 'is-system';
if (g.pacientes_count > 0) return 'is-em-uso';
return 'is-vazio';
}
// ── Stats ─────────────────────────────────────────────────
const stats = computed(() => {
const all = grupos.value;
const meus = all.filter((g) => !g.is_system).length;
const sistema = all.filter((g) => g.is_system).length;
const emUso = all.filter((g) => (g.pacientes_count || 0) > 0).length;
return [
{ key: 'total', label: 'Total', value: all.length, cls: 'neutral' },
{ key: 'meus', label: 'Meus', value: meus, cls: meus > 0 ? 'accent' : 'neutral' },
{ key: 'uso', label: 'Em uso', value: emUso, cls: emUso > 0 ? 'ok' : 'neutral' },
{ key: 'sistema', label: 'Sistema', value: sistema, cls: sistema > 0 ? 'info' : 'neutral' }
];
});
const TIPO_FILTER_OPTIONS = [
{ key: 'system', label: 'Sistema', icon: 'pi pi-shield' },
{ key: 'mine', label: 'Meus', icon: 'pi pi-user' }
];
const USO_FILTER_OPTIONS = [
{ key: 'em_uso', label: 'Em uso', icon: 'pi pi-users' },
{ key: 'vazio', label: 'Vazios', icon: 'pi pi-circle' }
];
function toggleTipoFilter(t) {
tipoFilter.value = tipoFilter.value === t ? '' : t;
}
function toggleUsoFilter(u) {
usoFilter.value = usoFilter.value === u ? '' : u;
}
const hasActiveFilters = computed(() =>
!!(busca.value || tipoFilter.value || usoFilter.value)
);
function clearAllFilters() {
busca.value = '';
tipoFilter.value = '';
usoFilter.value = '';
}
// ── Filtragem ─────────────────────────────────────────────
const filtered = computed(() => {
let list = grupos.value;
if (tipoFilter.value === 'system') list = list.filter((g) => !!g.is_system);
if (tipoFilter.value === 'mine') list = list.filter((g) => !g.is_system);
if (usoFilter.value === 'em_uso') list = list.filter((g) => (g.pacientes_count || 0) > 0);
if (usoFilter.value === 'vazio') list = list.filter((g) => (g.pacientes_count || 0) === 0);
const q = String(busca.value || '').trim().toLowerCase();
if (q) {
list = list.filter((g) => String(g.nome || '').toLowerCase().includes(q));
}
return list;
});
// ── Paginação compartilhada (DataTable + grid) ────────────
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
const rowsMG = ref(10);
const firstMG = ref(0);
function onPage(event) {
firstMG.value = event.first;
rowsMG.value = event.rows;
}
watch([busca, tipoFilter, usoFilter], () => { firstMG.value = 0; });
const pagedItems = computed(() =>
filtered.value.slice(firstMG.value, firstMG.value + rowsMG.value)
);
// ── View mode (list / grid) ───────────────────────────────
const VIEW_MODE_KEY = 'mg.viewMode.v1';
const viewMode = ref('list');
try {
const saved = localStorage.getItem(VIEW_MODE_KEY);
if (saved === 'list' || saved === 'grid') viewMode.value = saved;
} catch (_) { /* localStorage indisponível — mantém default */ }
function setViewMode(m) {
if (m !== 'list' && m !== 'grid') return;
viewMode.value = m;
try { localStorage.setItem(VIEW_MODE_KEY, m); } catch (_) { /* noop */ }
}
// ── Auth/tenant ───────────────────────────────────────────
async function getOwnerId() {
const { data } = await supabase.auth.getUser();
if (!data?.user?.id) throw new Error('Sessão não inicializada.');
return data.user.id;
}
async function getTenantId() {
if (typeof tenantStore.ensureLoaded === 'function') await tenantStore.ensureLoaded();
const tid = tenantStore.activeTenantId || tenantStore.tenantId;
if (!tid) throw new Error('Tenant não inicializado.');
return tid;
}
// ── Fetch ─────────────────────────────────────────────────
async function load() {
loading.value = true;
try {
const tenantId = await getTenantId();
const [{ data: gData, error: gErr }, { data: vData }] = await Promise.all([
supabase.from('patient_groups')
.select('id, owner_id, tenant_id, nome, cor, is_system, is_active, created_at')
.eq('tenant_id', tenantId)
.order('nome', { ascending: true }),
supabase.from('patient_group_patient').select('patient_group_id')
]);
if (gErr) throw gErr;
// Conta vínculos por grupo no client.
const map = new Map();
for (const v of vData || []) {
const id = v.patient_group_id;
map.set(id, (map.get(id) || 0) + 1);
}
counts.value = map;
grupos.value = (gData || []).map((g) => ({
...g,
pacientes_count: map.get(g.id) || 0
}));
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro ao carregar grupos', detail: e?.message, life: 4500 });
} finally {
loading.value = false;
}
}
onMounted(() => {
if (typeof window !== 'undefined' && window.matchMedia) {
_mqMobile = window.matchMedia('(max-width: 1023px)');
isMobile.value = _mqMobile.matches;
_mqMobile.addEventListener('change', _onMqMobileChange);
}
load();
});
onBeforeUnmount(() => {
if (_mqMobile) _mqMobile.removeEventListener('change', _onMqMobileChange);
});
// ── CRUD ──────────────────────────────────────────────────
const dlgOpen = ref(false);
const dlgMode = ref('create');
const dlgForm = ref({ id: '', nome: '', cor: '#6366F1' });
const dlgError = ref('');
const PRESET_COLORS = ['6366f1', '8b5cf6', 'ec4899', 'ef4444', 'f97316', 'eab308', '22c55e', '14b8a6', '3b82f6', '06b6d4', '64748b', '292524'];
function abrirCriar() {
dlgMode.value = 'create';
dlgForm.value = { id: '', nome: '', cor: '#6366F1' };
dlgError.value = '';
dlgOpen.value = true;
}
function abrirEditar(row) {
if (row.is_system) {
toast.add({ severity: 'info', summary: 'Grupo do sistema', detail: 'Não dá pra editar grupos do sistema.', life: 2500 });
return;
}
dlgMode.value = 'edit';
dlgForm.value = {
id: row.id,
nome: row.nome || '',
cor: row.cor ? (row.cor.startsWith('#') ? row.cor : '#' + row.cor) : '#6366F1'
};
dlgError.value = '';
dlgOpen.value = true;
}
function onRowClick(event) {
if (event?.data && !event.data.is_system) abrirEditar(event.data);
}
async function salvar() {
const nome = String(dlgForm.value.nome || '').trim();
if (!nome) {
dlgError.value = 'Informe um nome.';
return;
}
saving.value = true;
dlgError.value = '';
try {
const ownerId = await getOwnerId();
const tenantId = await getTenantId();
const cor = dlgForm.value.cor.startsWith('#') ? dlgForm.value.cor : '#' + dlgForm.value.cor;
if (dlgMode.value === 'create') {
const { error } = await supabase.from('patient_groups').insert({
owner_id: ownerId, tenant_id: tenantId,
nome, cor, is_system: false, is_active: true
});
if (error) throw error;
toast.add({ severity: 'success', summary: 'Grupo criado', life: 2200 });
} else {
const { error } = await supabase.from('patient_groups')
.update({ nome, cor })
.eq('id', dlgForm.value.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Grupo atualizado', life: 2200 });
}
dlgOpen.value = false;
await load();
} catch (e) {
const msg = e?.message || '';
dlgError.value = (e?.code === '23505' || /duplicate/i.test(msg)) ? 'Já existe um grupo com esse nome.' : (msg || 'Falha ao salvar.');
} finally {
saving.value = false;
}
}
function confirmarExcluir(row) {
if (row.is_system) return;
confirm.require({
message: `Excluir o grupo "${row.nome}"? Os pacientes serão desvinculados.`,
header: 'Confirmar exclusão',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Excluir',
rejectLabel: 'Cancelar',
acceptSeverity: 'danger',
accept: () => excluir(row)
});
}
async function excluir(row) {
saving.value = true;
try {
await supabase.from('patient_group_patient').delete().eq('patient_group_id', row.id);
const { error } = await supabase.from('patient_groups').delete().eq('id', row.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Grupo excluído', life: 2200 });
await load();
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao excluir.', life: 4000 });
} finally {
saving.value = false;
}
}
// ── Helpers de formatação (pra dialog "Pacientes do grupo") ─────
function initials(name) {
const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
if (!parts.length) return '—';
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
function fmtPhone(v) {
const d = String(v ?? '').replace(/\D/g, '');
if (!d) return '—';
if (d.length === 11) return `(${d.slice(0, 2)}) ${d.slice(2, 7)}-${d.slice(7)}`;
if (d.length === 10) return `(${d.slice(0, 2)}) ${d.slice(2, 6)}-${d.slice(6)}`;
return d;
}
// ── Dialog: Pacientes do grupo ──────────────────────────────────
// Espelha o pattern de GruposPacientesPage (layout Rail): click na coluna
// "Pacientes" abre dialog com a lista vinculada via patient_group_patient.
const patientsDialog = reactive({
open: false,
loading: false,
error: '',
group: null,
items: [],
search: ''
});
const patientsDialogFiltered = computed(() => {
const s = String(patientsDialog.search || '').toLowerCase().trim();
if (!s) return patientsDialog.items || [];
return (patientsDialog.items || []).filter((p) =>
String(p.full_name || '').toLowerCase().includes(s) ||
String(p.email || '').toLowerCase().includes(s) ||
String(p.phone || '').toLowerCase().includes(s)
);
});
// Cor do grupo em formato hex (#xxxxxx) — usada no header colorido,
// pills, borda do dialog. Default pra primary se sem cor.
const patientsGroupHex = computed(() => {
if (!patientsDialog.group) return '#6366f1';
const c = patientsDialog.group.cor || '#6366f1';
return c.startsWith('#') ? c : '#' + c;
});
async function openGroupPatientsModal(groupRow) {
if (!groupRow) return;
patientsDialog.open = true;
patientsDialog.loading = true;
patientsDialog.error = '';
patientsDialog.group = groupRow;
patientsDialog.items = [];
patientsDialog.search = '';
try {
const { data, error } = await supabase
.from('patient_group_patient')
.select('patient_id, patient:patients(id, nome_completo, email_principal, telefone, avatar_url)')
.eq('patient_group_id', groupRow.id);
if (error) throw error;
patientsDialog.items = (data || [])
.map((r) => r.patient)
.filter(Boolean)
.map((p) => ({
id: p.id,
full_name: p.nome_completo || '—',
email: p.email_principal || '—',
phone: p.telefone || '—',
avatar_url: p.avatar_url || null
}))
.sort((a, b) => String(a.full_name).localeCompare(String(b.full_name), 'pt-BR'));
} catch (e) {
patientsDialog.error = e?.message || 'Erro ao carregar pacientes do grupo.';
} finally {
patientsDialog.loading = false;
}
}
// Edição de paciente abre o PatientCadastroDialog reutilizado.
const editPatientId = ref(null);
const editPatientDialog = ref(false);
function abrirPaciente(patient) {
if (!patient?.id) return;
editPatientId.value = String(patient.id);
editPatientDialog.value = true;
}
watch(editPatientDialog, (isOpen) => {
if (!isOpen) editPatientId.value = null;
});
</script>
<template>
<aside
class="mg-mobile-drawer"
:class="{ 'is-open': drawerOpen }"
v-show="isMobile"
aria-label="Estatísticas e filtros"
>
<div id="mg-mobile-drawer-target" class="mg-mobile-drawer__scroll" />
</aside>
<Transition name="mg-drawer-fade">
<div
v-if="isMobile && drawerOpen"
class="mg-mobile-drawer__backdrop"
@click="fecharDrawer"
/>
</Transition>
<section class="mg-page">
<header class="mg-page__head">
<button
class="mg-menu-btn mg-menu-btn--mobile-only"
v-tooltip.bottom="'Estatísticas & filtros'"
@click="toggleDrawer"
>
<i class="pi pi-bars" />
<span>Menu Grupos</span>
</button>
<div class="mg-page__title">
<i class="pi pi-th-large mg-page__title-icon" />
<span>Grupos</span>
<span class="mg-page__count">{{ filtered.length }}</span>
</div>
<div class="mg-page__actions">
<button
class="mg-act-btn mg-act-btn--primary"
v-tooltip.bottom="'Novo grupo'"
:disabled="loading"
@click="abrirCriar"
>
<i :class="saving ? 'pi pi-spin pi-spinner' : 'pi pi-plus'" />
<span>Novo</span>
</button>
<button
class="mg-head-btn"
v-tooltip.bottom="'Recarregar'"
:disabled="loading"
@click="load"
>
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
</button>
<button class="mg-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
<i class="pi pi-times" />
</button>
</div>
</header>
<!-- Subheader explicativo (blueprint §9) -->
<div class="mg-subheader">
<i class="pi pi-info-circle mg-subheader__icon" />
<span class="mg-subheader__text">
Coleções pra organizar pacientes (ex: Adolescentes, Casais, Avulsos).
<strong>Crie grupos próprios</strong> com cor identificadora eles aparecem
como filtro rápido na lista de pacientes.
</span>
</div>
<div class="mg-body">
<Teleport to="#mg-mobile-drawer-target" :disabled="!isMobile">
<aside class="mg-side">
<div class="mg-side__scroll">
<!-- Stats -->
<div class="mg-w mg-w--side">
<div class="mg-w__head">
<span class="mg-w__title"><i class="pi pi-chart-bar" /> Estatísticas</span>
</div>
<div class="mg-stats">
<div
v-for="s in stats"
:key="s.key"
class="mg-stat"
:class="`is-${s.cls}`"
>
<div class="mg-stat__val">{{ s.value }}</div>
<div class="mg-stat__lbl">{{ s.label }}</div>
</div>
</div>
</div>
<!-- Filtro de tipo -->
<div class="mg-w mg-w--side">
<div class="mg-w__head">
<span class="mg-w__title"><i class="pi pi-filter" /> Tipo</span>
<button
v-if="tipoFilter"
class="mg-side__clear-inline"
v-tooltip.top="'Limpar filtro de tipo'"
aria-label="Limpar filtro de tipo"
@click="tipoFilter = ''"
>
<i class="pi pi-times" />
</button>
</div>
<div class="mg-side__list">
<button
v-for="o in TIPO_FILTER_OPTIONS"
:key="o.key"
class="mg-side__item"
:class="[`is-tipo-${o.key}`, { 'is-active': tipoFilter === o.key }]"
@click="toggleTipoFilter(o.key)"
>
<i :class="o.icon" />
<span>{{ o.label }}</span>
</button>
</div>
</div>
<!-- Filtro de uso -->
<div class="mg-w mg-w--side">
<div class="mg-w__head">
<span class="mg-w__title"><i class="pi pi-users" /> Uso</span>
<button
v-if="usoFilter"
class="mg-side__clear-inline"
v-tooltip.top="'Limpar filtro de uso'"
aria-label="Limpar filtro de uso"
@click="usoFilter = ''"
>
<i class="pi pi-times" />
</button>
</div>
<div class="mg-side__list">
<button
v-for="o in USO_FILTER_OPTIONS"
:key="o.key"
class="mg-side__item"
:class="[`is-uso-${o.key}`, { 'is-active': usoFilter === o.key }]"
@click="toggleUsoFilter(o.key)"
>
<i :class="o.icon" />
<span>{{ o.label }}</span>
</button>
</div>
</div>
</div>
<!-- Footer fixo com Limpar filtros (Transition fade+collapse) -->
<Transition name="mg-clear">
<div v-if="hasActiveFilters" class="mg-side__footer">
<button class="mg-side__clear-all" @click="clearAllFilters">
<i class="pi pi-filter-slash" />
<span>Limpar filtros</span>
</button>
</div>
</Transition>
</aside>
</Teleport>
<div class="mg-main">
<!-- Toolbar (busca + view toggle) -->
<div class="mg-toolbar">
<div class="mg-search">
<i class="pi pi-search mg-search__icon" />
<input
v-model="busca"
type="text"
placeholder="Buscar por nome…"
class="mg-search__input"
/>
<button
v-if="busca"
class="mg-search__clear"
v-tooltip.bottom="'Limpar busca'"
@click="busca = ''"
>
<i class="pi pi-times" />
</button>
</div>
<div class="mg-view-toggle" role="group" aria-label="Visualização">
<button
class="mg-view-toggle__btn"
:class="{ 'is-active': viewMode === 'list' }"
v-tooltip.bottom="'Lista'"
aria-label="Lista"
@click="setViewMode('list')"
>
<i class="pi pi-list" />
</button>
<button
class="mg-view-toggle__btn"
:class="{ 'is-active': viewMode === 'grid' }"
v-tooltip.bottom="'Grade'"
aria-label="Grade"
@click="setViewMode('grid')"
>
<i class="pi pi-th-large" />
</button>
</div>
</div>
<!-- View Lista (DataTable) row design preservado:
coluna "Grupo" larga renderiza dot + nome + badge Sistema
(mesma cara do card antigo); coluna "Pacientes" 160px com
contagem; coluna "Ações" frozen 100px com edit + trash. -->
<DataTable
v-if="viewMode === 'list'"
:value="filtered"
:loading="loading"
dataKey="id"
paginator
:rows="rowsMG"
:first="firstMG"
:rowsPerPageOptions="PAGE_SIZE_OPTIONS"
paginatorTemplate="RowsPerPageDropdown FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink"
currentPageReportTemplate="{first}{last} de {totalRecords}"
:rowClass="rowStatusClass"
selectionMode="single"
scrollable
scrollHeight="flex"
tableStyle="min-width: 600px"
class="mg-table"
@row-click="onRowClick"
@page="onPage"
>
<Column header="Grupo" style="min-width: 280px">
<template #body="{ data }">
<div class="mg-row__group">
<span
class="mg-row__dot"
:style="{ background: data.cor || '#6366f1' }"
/>
<div class="mg-row__main">
<div class="mg-row__name-row">
<span class="mg-row__name">{{ data.nome }}</span>
<span v-if="data.is_system" class="mg-row__badge mg-row__badge--system">
<i class="pi pi-shield" /> Sistema
</span>
</div>
</div>
</div>
</template>
</Column>
<Column header="Pacientes" style="width: 180px; min-width: 160px">
<template #body="{ data }">
<button
type="button"
class="mg-row__count-btn"
:class="{ 'is-empty': data.pacientes_count === 0 }"
:disabled="data.pacientes_count === 0"
v-tooltip.top="data.pacientes_count > 0 ? 'Ver pacientes do grupo' : 'Sem pacientes vinculados'"
aria-label="Ver pacientes do grupo"
@click.stop="openGroupPatientsModal(data)"
>
<i class="pi pi-users" />
<span>
{{ data.pacientes_count }}
{{ data.pacientes_count === 1 ? 'paciente' : 'pacientes' }}
</span>
</button>
</template>
</Column>
<Column
header=""
:style="{ width: '100px', maxWidth: '100px', minWidth: '100px' }"
frozen
alignFrozen="right"
class="mg-col-acoes"
>
<template #body="{ data }">
<div class="mg-row__actions" @click.stop>
<button
v-if="!data.is_system"
class="mg-row__btn"
v-tooltip.left="'Editar'"
aria-label="Editar"
@click.stop="abrirEditar(data)"
>
<i class="pi pi-pencil" />
</button>
<button
v-if="!data.is_system"
class="mg-row__btn mg-row__btn--danger"
v-tooltip.left="'Excluir'"
aria-label="Excluir"
@click.stop="confirmarExcluir(data)"
>
<i class="pi pi-trash" />
</button>
<span v-else class="mg-row__locked" v-tooltip.left="'Grupo do sistema (bloqueado)'">
<i class="pi pi-lock" />
</span>
</div>
</template>
</Column>
<template #empty>
<div class="mg-empty">
<i class="pi pi-th-large mg-empty__icon" />
<div class="mg-empty__title">Nenhum grupo encontrado</div>
<div class="mg-empty__hint">
<template v-if="busca || tipoFilter || usoFilter">
Ajuste os filtros pra ver mais resultados.
</template>
<template v-else>
Crie seu primeiro grupo pra organizar pacientes.
</template>
</div>
<button class="mg-act-btn mg-act-btn--primary mg-empty__btn" @click="abrirCriar">
<i class="pi pi-plus" />
<span>Novo grupo</span>
</button>
</div>
</template>
<template #loading>
<div class="mg-table__loading">
<i class="pi pi-spin pi-spinner" />
<span>Carregando grupos…</span>
</div>
</template>
</DataTable>
<!-- View Grade — cards num CSS grid; mesmo conteúdo do row
em formato vertical. Paginator standalone compartilha
rowsMG/firstMG com a list view. -->
<div v-else-if="viewMode === 'grid'" class="mg-grid-wrap">
<div v-if="loading && filtered.length === 0" class="mg-table__loading mg-grid__loading">
<i class="pi pi-spin pi-spinner" />
<span>Carregando grupos…</span>
</div>
<div v-else-if="filtered.length === 0" class="mg-empty">
<i class="pi pi-th-large mg-empty__icon" />
<div class="mg-empty__title">Nenhum grupo encontrado</div>
<div class="mg-empty__hint">
<template v-if="busca || tipoFilter || usoFilter">
Ajuste os filtros pra ver mais resultados.
</template>
<template v-else>
Crie seu primeiro grupo pra organizar pacientes.
</template>
</div>
</div>
<div v-else class="mg-grid">
<div
v-for="g in pagedItems"
:key="g.id"
class="mg-grid__card"
:class="rowStatusClass(g)"
role="button"
tabindex="0"
@click="abrirEditar(g)"
@keydown.enter.prevent="abrirEditar(g)"
@keydown.space.prevent="abrirEditar(g)"
>
<div class="mg-grid__top">
<span
class="mg-grid__dot"
:style="{ background: g.cor || '#6366f1' }"
/>
<span v-if="g.is_system" class="mg-row__badge mg-row__badge--system">
<i class="pi pi-shield" /> Sistema
</span>
</div>
<div class="mg-grid__name">{{ g.nome }}</div>
<div class="mg-grid__meta" @click.stop>
<button
type="button"
class="mg-row__count-btn"
:class="{ 'is-empty': g.pacientes_count === 0 }"
:disabled="g.pacientes_count === 0"
v-tooltip.top="g.pacientes_count > 0 ? 'Ver pacientes do grupo' : 'Sem pacientes vinculados'"
aria-label="Ver pacientes do grupo"
@click.stop="openGroupPatientsModal(g)"
>
<i class="pi pi-users" />
<span>
{{ g.pacientes_count }}
{{ g.pacientes_count === 1 ? 'paciente' : 'pacientes' }}
</span>
</button>
</div>
<div class="mg-grid__footer" @click.stop>
<button
v-if="!g.is_system"
class="mg-row__btn"
v-tooltip.top="'Editar'"
aria-label="Editar"
@click.stop="abrirEditar(g)"
>
<i class="pi pi-pencil" />
</button>
<button
v-if="!g.is_system"
class="mg-row__btn mg-row__btn--danger"
v-tooltip.top="'Excluir'"
aria-label="Excluir"
@click.stop="confirmarExcluir(g)"
>
<i class="pi pi-trash" />
</button>
<span v-else class="mg-row__locked" v-tooltip.top="'Grupo do sistema (bloqueado)'">
<i class="pi pi-lock" />
</span>
</div>
</div>
</div>
<Paginator
v-if="filtered.length > 0"
class="mg-paginator"
:rows="rowsMG"
:totalRecords="filtered.length"
:first="firstMG"
:rowsPerPageOptions="PAGE_SIZE_OPTIONS"
template="RowsPerPageDropdown FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink"
currentPageReportTemplate="{first}{last} de {totalRecords}"
@page="onPage"
/>
</div>
</div>
</div>
<!-- Dialog: criar/editar grupo — pattern espelhado do
PatientsCadastroPage Identidade (FloatLabel + IconField +
variant="filled" + section dividers uppercase). -->
<Dialog
v-model:visible="dlgOpen"
modal
dismissable-mask
:draggable="false"
:style="{ width: '480px', maxWidth: '94vw' }"
:header="dlgMode === 'create' ? 'Novo grupo' : 'Editar grupo'"
>
<div class="p-1">
<!-- ── Identificação ─────────────────────────── -->
<div class="flex items-center gap-2 mb-5">
<span class="text-[0.7rem] font-bold uppercase tracking-widest text-[var(--p-primary-color)]">
Grupo
</span>
<div class="flex-1 h-px bg-[color-mix(in_srgb,var(--p-primary-color)_25%,transparent)]" />
</div>
<div class="mb-6">
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-folder" />
<InputText
id="grp-nome"
v-model="dlgForm.nome"
class="w-full"
variant="filled"
autofocus
@keydown.enter="salvar"
/>
</IconField>
<label for="grp-nome">Nome do grupo *</label>
</FloatLabel>
</div>
<!-- ── Cor identificadora ────────────────────── -->
<div class="flex items-center gap-2 mb-4">
<span class="text-[0.7rem] font-bold uppercase tracking-widest text-[var(--p-primary-color)]">
Cor
</span>
<div class="flex-1 h-px bg-[color-mix(in_srgb,var(--p-primary-color)_25%,transparent)]" />
</div>
<div class="flex items-center gap-3 mb-4">
<input
v-model="dlgForm.cor"
type="color"
class="h-10 w-14 rounded-lg border border-[var(--surface-border)] bg-transparent cursor-pointer"
/>
<span class="text-[0.82rem] font-mono text-[var(--text-color-secondary)] uppercase">
{{ dlgForm.cor }}
</span>
</div>
<div class="flex flex-wrap gap-2">
<button
v-for="c in PRESET_COLORS"
:key="c"
type="button"
class="mg-color-preset"
:class="{ 'is-selected': dlgForm.cor.toLowerCase() === '#' + c }"
:style="{ background: '#' + c }"
:aria-label="`Usar cor #${c}`"
@click="dlgForm.cor = '#' + c"
/>
</div>
<small
v-if="dlgError"
class="mt-4 text-[0.85rem] text-red-500 flex items-center gap-1.5"
>
<i class="pi pi-exclamation-circle text-[0.78rem]" />
<span>{{ dlgError }}</span>
</small>
</div>
<template #footer>
<Button
label="Cancelar"
severity="secondary"
text
:disabled="saving"
@click="dlgOpen = false"
/>
<Button
:label="saving ? 'Salvando…' : 'Salvar'"
icon="pi pi-check"
:loading="saving"
:disabled="!dlgForm.nome.trim() || saving"
@click="salvar"
/>
</template>
</Dialog>
<!-- Dialog: Pacientes do grupo (espelha o pattern de
GruposPacientesPage do layout Rail). Abre ao clicar na
coluna "Pacientes" da DataTable ou no contador da grid view.
Borda colorida com a cor do grupo + DataTable interno
com search, paginator e botão "Abrir" pra cada paciente. -->
<Dialog
v-model:visible="patientsDialog.open"
modal
dismissable-mask
:draggable="false"
:style="{ width: '720px', maxWidth: '94vw' }"
:pt="{
root: { style: `border: 2px solid ${patientsGroupHex}` },
header: { style: `border-bottom: 1px solid ${patientsGroupHex}30` },
pcCloseButton: { root: { class: 'mg-pdlg-close-btn' } }
}"
>
<template #header>
<div class="mg-pdlg__header">
<span class="mg-pdlg__avatar" :style="{ background: patientsGroupHex }">
{{ (patientsDialog.group?.nome || '?')[0].toUpperCase() }}
</span>
<div class="mg-pdlg__title">
<div class="mg-pdlg__name" :style="{ color: patientsGroupHex }">
Grupo — {{ patientsDialog.group?.nome }}
</div>
<div class="mg-pdlg__sub">
{{ patientsDialog.items.length }}
paciente{{ patientsDialog.items.length !== 1 ? 's' : '' }}
</div>
</div>
</div>
</template>
<div class="mg-pdlg__body">
<!-- Toolbar: busca + count pill -->
<div class="mg-pdlg__toolbar">
<div class="mg-search mg-pdlg__search">
<i class="pi pi-search mg-search__icon" />
<input
v-model="patientsDialog.search"
type="text"
placeholder="Buscar paciente"
class="mg-search__input"
:disabled="patientsDialog.loading"
/>
<button
v-if="patientsDialog.search"
class="mg-search__clear"
v-tooltip.bottom="'Limpar busca'"
@click="patientsDialog.search = ''"
>
<i class="pi pi-times" />
</button>
</div>
<span
v-if="!patientsDialog.loading"
class="mg-pdlg__count-pill"
:style="{ background: `${patientsGroupHex}18`, color: patientsGroupHex }"
>
{{ patientsDialog.items.length }}
{{ patientsDialog.items.length === 1 ? 'paciente' : 'pacientes' }}
</span>
</div>
<!-- Loading -->
<div
v-if="patientsDialog.loading"
class="mg-pdlg__loading"
:style="{ color: patientsGroupHex }"
>
<i class="pi pi-spin pi-spinner" />
<span>Carregando pacientes…</span>
</div>
<!-- Erro -->
<div v-else-if="patientsDialog.error" class="mg-pdlg__error">
<i class="pi pi-exclamation-triangle" />
<span>{{ patientsDialog.error }}</span>
</div>
<!-- Vazio (grupo sem pacientes) -->
<div v-else-if="patientsDialog.items.length === 0" class="mg-pdlg__empty">
<div
class="mg-pdlg__empty-icon"
:style="{ background: `${patientsGroupHex}18`, color: patientsGroupHex }"
>
<i class="pi pi-users" />
</div>
<div class="mg-pdlg__empty-title">Nenhum paciente neste grupo</div>
<div class="mg-pdlg__empty-hint">
Associe pacientes a este grupo na página de pacientes.
</div>
</div>
<!-- Tabela de pacientes -->
<DataTable
v-else
:value="patientsDialogFiltered"
dataKey="id"
paginator
:rows="8"
:rowsPerPageOptions="[8, 15, 30]"
paginatorTemplate="RowsPerPageDropdown FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink"
currentPageReportTemplate="{first}{last} de {totalRecords}"
stripedRows
responsiveLayout="scroll"
class="mg-pdlg__table"
>
<Column header="Paciente" sortable>
<template #body="{ data }">
<div class="mg-pdlg__patient">
<span
class="mg-pdlg__avatar mg-pdlg__avatar--sm"
:style="data.avatar_url ? null : { background: `${patientsGroupHex}25`, color: patientsGroupHex }"
>
<img v-if="data.avatar_url" :src="data.avatar_url" :alt="data.full_name" />
<span v-else>{{ initials(data.full_name) }}</span>
</span>
<div class="mg-pdlg__patient-text">
<span class="mg-pdlg__patient-name">{{ data.full_name }}</span>
<span class="mg-pdlg__patient-email">{{ data.email || '—' }}</span>
</div>
</div>
</template>
</Column>
<Column header="Telefone" style="min-width: 11rem">
<template #body="{ data }">
<span class="mg-pdlg__phone">{{ fmtPhone(data.phone) }}</span>
</template>
</Column>
<Column header="" style="width: 7rem">
<template #body="{ data }">
<button
type="button"
class="mg-pdlg__open-btn"
:style="{ borderColor: patientsGroupHex, color: patientsGroupHex }"
v-tooltip.left="'Abrir prontuário'"
aria-label="Abrir prontuário do paciente"
@click="abrirPaciente(data)"
>
<i class="pi pi-external-link" />
<span>Abrir</span>
</button>
</template>
</Column>
<template #empty>
<div class="mg-pdlg__empty">
<i class="pi pi-search mg-pdlg__empty-icon-pi" />
<div class="mg-pdlg__empty-title">Nenhum resultado</div>
<div class="mg-pdlg__empty-hint">
Nenhum paciente corresponde à busca.
</div>
<button
class="mg-pdlg__inline-clear-btn"
@click="patientsDialog.search = ''"
>
<i class="pi pi-filter-slash" />
<span>Limpar busca</span>
</button>
</div>
</template>
</DataTable>
</div>
</Dialog>
<!-- Edição de paciente vinda do dialog "Pacientes do grupo".
v-model controla open/close; quando fecha, limpa o id (watch). -->
<PatientCadastroDialog v-model="editPatientDialog" :patient-id="editPatientId" />
</section>
</template>
<style scoped>
/* ─── Page chrome (espelha MelissaCadastrosRecebidos / MelissaCompromissos) ─── */
.mg-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: mg-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
@keyframes mg-page-enter {
from { opacity: 0; transform: scale(0.985); }
to { opacity: 1; transform: scale(1); }
}
.mg-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;
}
.mg-page__title {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
font-weight: 500;
}
.mg-page__title-icon {
color: var(--p-primary-color);
font-size: 1.05rem;
}
.mg-page__title > span:not(.mg-page__count) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mg-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;
}
.mg-page__actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.mg-close, .mg-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;
}
.mg-close:hover, .mg-head-btn:hover { background: var(--m-bg-soft-hover); }
.mg-head-btn > i { font-size: 0.85rem; }
.mg-act-btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 12px;
border-radius: 9px;
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
transition: background-color 140ms ease, transform 140ms ease;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
}
.mg-act-btn--primary {
background: var(--m-accent);
border-color: var(--m-accent);
color: white;
}
.mg-act-btn--primary:hover {
background: color-mix(in srgb, var(--m-accent) 88%, white);
transform: translateY(-1px);
}
.mg-act-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.mg-act-btn > i { font-size: 0.78rem; }
.mg-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;
}
.mg-menu-btn:hover {
background: color-mix(in srgb, var(--m-accent) 88%, white);
transform: translateY(-1px);
}
/* Subheader (blueprint §9) */
.mg-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;
}
.mg-subheader__icon {
color: var(--p-primary-color);
font-size: 0.92rem;
flex-shrink: 0;
margin-top: 1px;
}
.mg-subheader__text { flex: 1; min-width: 0; }
.mg-subheader__text strong { color: var(--m-text); font-weight: 600; }
.mg-body {
flex: 1;
display: flex;
min-height: 0;
gap: 0;
padding: 0;
}
/* ─── Sidebar (2 zonas: __scroll + __footer fixo) ─── */
.mg-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;
}
.mg-side__scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.mg-side__scroll::-webkit-scrollbar { width: 5px; }
.mg-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mg-side__footer {
flex-shrink: 0;
padding: 12px;
background: var(--m-bg-soft);
border-top: 1px solid var(--m-border);
}
.mg-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;
}
.mg-side__clear-all:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
color: var(--m-text);
}
.mg-side__clear-all > i {
font-size: 0.78rem;
color: var(--m-text-muted);
transition: color 140ms ease;
}
.mg-side__clear-all:hover > i { color: var(--m-text); }
/* X inline ao lado do título de cada filter card */
.mg-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;
}
.mg-side__clear-inline:hover {
background: rgba(220, 38, 38, 0.10);
border-color: rgba(220, 38, 38, 0.55);
}
.mg-side__clear-inline > i { font-size: 0.6rem; }
/* Cards na sidebar */
.mg-w {
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
border-radius: 12px;
padding: 12px;
}
.mg-w--side {
margin: 12px 12px 0;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.mg-w--side:last-of-type { margin-bottom: 12px; }
.mg-w__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.mg-w__title {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--m-text-muted);
font-weight: 600;
}
.mg-w__title > i { color: var(--m-text-muted); font-size: 0.7rem; }
.mg-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.mg-stat {
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-radius: 10px;
padding: 8px 10px;
}
.mg-stat__val {
font-size: 1.1rem;
font-weight: 600;
line-height: 1.1;
}
.mg-stat__lbl {
font-size: 0.65rem;
color: var(--m-text-muted);
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.mg-stat.is-ok .mg-stat__val { color: rgb(22, 163, 74); } /* green-600 */
.mg-stat.is-info .mg-stat__val { color: rgb(37, 99, 235); } /* blue-600 */
.mg-stat.is-accent .mg-stat__val { color: var(--m-accent); }
/* ─── Filter buttons (blueprint §8) ─── */
.mg-side__list {
display: flex;
flex-direction: column;
gap: 4px;
}
.mg-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, box-shadow 140ms ease;
}
.mg-side__item > i {
font-size: 0.78rem;
width: 14px;
text-align: center;
}
/* Tipo: Sistema = blue-600, Meus = accent */
.mg-side__item.is-tipo-system {
background: rgba(37, 99, 235, 0.05);
border-color: rgba(37, 99, 235, 0.18);
}
.mg-side__item.is-tipo-system > i { color: rgb(37, 99, 235); }
.mg-side__item.is-tipo-system:hover {
background: rgba(37, 99, 235, 0.10);
border-color: rgba(37, 99, 235, 0.30);
}
.mg-side__item.is-active.is-tipo-system {
background: rgba(37, 99, 235, 0.16);
border-color: rgba(37, 99, 235, 0.55);
box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.35);
}
.mg-side__item.is-tipo-mine {
background: color-mix(in srgb, var(--m-accent) 5%, transparent);
border-color: color-mix(in srgb, var(--m-accent) 18%, transparent);
}
.mg-side__item.is-tipo-mine > i { color: var(--m-accent); }
.mg-side__item.is-tipo-mine:hover {
background: color-mix(in srgb, var(--m-accent) 10%, transparent);
border-color: color-mix(in srgb, var(--m-accent) 30%, transparent);
}
.mg-side__item.is-active.is-tipo-mine {
background: color-mix(in srgb, var(--m-accent) 16%, transparent);
border-color: color-mix(in srgb, var(--m-accent) 55%, transparent);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--m-accent) 35%, transparent);
}
/* Uso: Em uso = green-600, Vazio = amber-600 (warn) */
.mg-side__item.is-uso-em_uso {
background: rgba(22, 163, 74, 0.05);
border-color: rgba(22, 163, 74, 0.18);
}
.mg-side__item.is-uso-em_uso > i { color: rgb(22, 163, 74); }
.mg-side__item.is-uso-em_uso:hover {
background: rgba(22, 163, 74, 0.10);
border-color: rgba(22, 163, 74, 0.30);
}
.mg-side__item.is-active.is-uso-em_uso {
background: rgba(22, 163, 74, 0.16);
border-color: rgba(22, 163, 74, 0.55);
box-shadow: 0 0 0 1px rgba(22, 163, 74, 0.35);
}
.mg-side__item.is-uso-vazio {
background: rgba(217, 119, 6, 0.05);
border-color: rgba(217, 119, 6, 0.18);
}
.mg-side__item.is-uso-vazio > i { color: rgb(217, 119, 6); }
.mg-side__item.is-uso-vazio:hover {
background: rgba(217, 119, 6, 0.10);
border-color: rgba(217, 119, 6, 0.30);
}
.mg-side__item.is-active.is-uso-vazio {
background: rgba(217, 119, 6, 0.16);
border-color: rgba(217, 119, 6, 0.55);
box-shadow: 0 0 0 1px rgba(217, 119, 6, 0.35);
}
/* Transition do footer "Limpar filtros" */
.mg-clear-enter-active,
.mg-clear-leave-active {
transition: opacity 220ms ease, transform 220ms ease, max-height 240ms ease;
overflow: hidden;
}
.mg-clear-enter-from,
.mg-clear-leave-to {
opacity: 0;
transform: translateY(6px);
max-height: 0;
}
.mg-clear-enter-to,
.mg-clear-leave-from {
opacity: 1;
transform: translateY(0);
max-height: 80px;
}
/* ─── Main column ─── */
.mg-main {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
padding: 12px;
gap: 10px;
}
.mg-toolbar {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 10px;
}
.mg-search {
position: relative;
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
.mg-search__icon {
position: absolute;
left: 12px;
color: var(--m-text-muted);
font-size: 0.78rem;
pointer-events: none;
}
.mg-search__input {
width: 100%;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
color: var(--m-text);
padding: 9px 36px 9px 34px;
border-radius: 10px;
font-size: 0.85rem;
font-family: inherit;
outline: none;
transition: background-color 140ms ease, border-color 140ms ease;
}
.mg-search__input::placeholder { color: var(--m-text-faint); }
.mg-search__input:focus {
border-color: var(--m-border-strong);
background: var(--m-bg-medium);
}
.mg-search__clear {
position: absolute;
right: 8px;
width: 22px;
height: 22px;
display: grid;
place-items: center;
background: transparent;
border: none;
color: var(--m-text-muted);
border-radius: 6px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease, color 140ms ease;
}
.mg-search__clear:hover {
background: var(--m-bg-soft-hover);
color: var(--m-text);
}
.mg-search__clear > i { font-size: 0.7rem; }
.mg-view-toggle {
flex-shrink: 0;
display: inline-flex;
background: var(--m-bg-medium);
border: 1px solid var(--m-border);
border-radius: 10px;
padding: 2px;
gap: 2px;
}
.mg-view-toggle__btn {
width: 32px;
height: 32px;
display: grid;
place-items: center;
background: transparent;
border: none;
color: var(--m-text-muted);
border-radius: 8px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease, color 140ms ease;
}
.mg-view-toggle__btn:hover {
background: var(--m-bg-soft-hover);
color: var(--m-text);
}
.mg-view-toggle__btn.is-active {
background: var(--m-accent-soft);
color: var(--m-accent);
}
.mg-view-toggle__btn > i { font-size: 0.85rem; }
/* ─── DataTable ─── */
.mg-table {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.mg-table :deep(.p-datatable) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: transparent;
border: 1px solid var(--m-border);
border-radius: 10px;
overflow: hidden;
}
.mg-table :deep(.p-datatable-table-container) {
flex: 1;
min-height: 0;
background: transparent;
}
.mg-table :deep(.p-datatable-thead),
.mg-table :deep(.p-datatable-thead > tr) {
background: transparent !important;
}
.mg-table :deep(.p-datatable-thead > tr > th) {
background: var(--p-content-background) !important;
color: var(--m-text);
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 10px 14px;
border-bottom: 1px solid var(--m-border);
}
.mg-table :deep(.p-datatable-tbody > tr) {
background: transparent;
color: var(--m-text);
cursor: pointer;
transition: background-color 140ms ease;
border-left: 3px solid var(--m-border);
}
.mg-table :deep(.p-datatable-tbody > tr > td) {
padding: 10px 14px;
border-bottom: 1px solid var(--m-border);
background: transparent;
vertical-align: middle;
}
.mg-table :deep(.p-datatable-tbody > tr:hover) {
background: var(--m-bg-soft-hover);
}
.mg-table :deep(.p-datatable-tbody > tr.p-datatable-row-selected) {
background: var(--m-accent-soft);
}
/* Border-left: Sistema = azul, Em uso = verde, Vazio = sutil */
.mg-table :deep(.p-datatable-tbody > tr.is-system) { border-left-color: rgb(37, 99, 235); cursor: default; }
.mg-table :deep(.p-datatable-tbody > tr.is-em-uso) { border-left-color: rgb(22, 163, 74); }
.mg-table :deep(.p-datatable-tbody > tr.is-vazio) { border-left-color: var(--m-border-strong); }
.mg-table :deep(.p-datatable-loading-overlay) {
background: color-mix(in srgb, var(--m-bg-medium) 70%, transparent);
backdrop-filter: blur(2px);
}
.mg-table__loading {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--m-text);
font-size: 0.85rem;
}
/* Paginator (DataTable interno) */
.mg-table :deep(.p-paginator) {
background: var(--m-bg-medium);
border: none;
border-top: 1px solid var(--m-border);
padding: 8px 12px;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
}
.mg-table :deep(.p-paginator-current) {
color: var(--m-text-muted);
font-size: 0.78rem;
background: transparent;
border: none;
padding: 0 6px;
}
.mg-table :deep(.p-paginator-first),
.mg-table :deep(.p-paginator-prev),
.mg-table :deep(.p-paginator-next),
.mg-table :deep(.p-paginator-last),
.mg-table :deep(.p-paginator-page) {
min-width: 30px;
height: 30px;
color: var(--m-text);
background: transparent;
border: 1px solid var(--m-border);
border-radius: 8px;
font-size: 0.8rem;
transition: background-color 140ms ease, border-color 140ms ease;
}
.mg-table :deep(.p-paginator-page.p-paginator-page-selected) {
background: var(--m-accent-soft);
border-color: var(--m-accent-strong);
color: var(--m-accent);
}
.mg-table :deep(.p-paginator-first:not(.p-disabled):hover),
.mg-table :deep(.p-paginator-prev:not(.p-disabled):hover),
.mg-table :deep(.p-paginator-next:not(.p-disabled):hover),
.mg-table :deep(.p-paginator-last:not(.p-disabled):hover),
.mg-table :deep(.p-paginator-page:not(.p-paginator-page-selected):hover) {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mg-table :deep(.p-select) {
background: transparent;
border: 1px solid var(--m-border);
border-radius: 8px;
height: 30px;
font-size: 0.78rem;
color: var(--m-text);
display: inline-flex;
align-items: center;
}
.mg-table :deep(.p-select-label) {
padding: 0 8px;
color: var(--m-text);
font-size: 0.78rem;
display: flex;
align-items: center;
line-height: 1;
height: 100%;
background: transparent;
}
/* Coluna frozen "Ações" — bg sólido em ambos modos */
.mg-table :deep(td.p-datatable-frozen-column),
.mg-table :deep(th.p-datatable-frozen-column) {
background: var(--p-content-background) !important;
box-shadow: -3px 0 6px -3px rgba(0, 0, 0, 0.18);
z-index: 1;
}
.mg-table :deep(.p-datatable-tbody > tr:hover td.p-datatable-frozen-column) {
background: var(--m-bg-soft-hover);
}
.mg-table :deep(.p-datatable-tbody > tr.p-datatable-row-selected td.p-datatable-frozen-column) {
background: var(--m-accent-soft);
}
/* ─── Row content (design preservado do card antigo) ─── */
.mg-row__group {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.mg-row__dot {
width: 16px;
height: 16px;
border-radius: 50%;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--p-content-background) 60%, transparent);
}
.mg-row__main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.mg-row__name-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.mg-row__name {
font-size: 0.92rem;
font-weight: 600;
color: var(--m-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mg-row__count {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 0.78rem;
color: var(--m-text-muted);
}
.mg-row__count > i { font-size: 0.7rem; }
/* Botão "X pacientes" clicável (na coluna Pacientes da DataTable
e na meta do grid). Quando há pacientes, vira pill primary tinted
pra sinalizar que é clicável; quando vazio, fica neutro/disabled. */
.mg-row__count-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: transparent;
border: 1px solid color-mix(in srgb, var(--p-primary-color) 25%, var(--m-border));
color: var(--p-primary-color);
border-radius: 999px;
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 500;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
white-space: nowrap;
}
.mg-row__count-btn:hover:not(:disabled) {
background: color-mix(in srgb, var(--p-primary-color) 12%, transparent);
border-color: var(--p-primary-color);
}
.mg-row__count-btn:focus-visible {
outline: 2px solid var(--p-primary-color);
outline-offset: 2px;
}
.mg-row__count-btn.is-empty,
.mg-row__count-btn:disabled {
color: var(--m-text-muted);
border-color: var(--m-border);
cursor: default;
background: transparent;
}
.mg-row__count-btn > i { font-size: 0.7rem; }
.mg-row__badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.62rem;
font-weight: 600;
padding: 2px 8px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.04em;
border: 1px solid;
flex-shrink: 0;
}
.mg-row__badge--system {
color: rgb(37, 99, 235);
background: rgba(37, 99, 235, 0.12);
border-color: rgba(37, 99, 235, 0.3);
}
.mg-row__badge > i { font-size: 0.55rem; }
/* Coluna de ações */
.mg-row__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
flex-shrink: 0;
}
.mg-row__btn {
width: 28px;
height: 28px;
display: grid;
place-items: center;
background: var(--p-content-background);
border: 1px solid var(--m-border);
color: var(--m-text-muted);
border-radius: 8px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
flex-shrink: 0;
}
.mg-row__btn:hover {
background: color-mix(in srgb, var(--p-primary-color) 12%, var(--p-content-background));
border-color: var(--p-primary-color);
color: var(--p-primary-color);
}
.mg-row__btn--danger:hover {
background: rgba(220, 38, 38, 0.12);
border-color: rgba(220, 38, 38, 0.5);
color: rgb(220, 38, 38);
}
.mg-row__btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.mg-row__btn > i { font-size: 0.7rem; }
/* Lock icon (sistema, sem ações) */
.mg-row__locked {
width: 28px;
height: 28px;
display: grid;
place-items: center;
color: var(--m-text-faint);
font-size: 0.7rem;
}
/* ─── Grid view ─── */
.mg-grid-wrap {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 10px;
border: 1px solid var(--m-border);
border-radius: 10px;
overflow: hidden;
background: transparent;
}
.mg-grid {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 12px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 10px;
align-content: start;
}
.mg-grid::-webkit-scrollbar { width: 5px; }
.mg-grid::-webkit-scrollbar-thumb { background: var(--m-border-strong); border-radius: 3px; }
.mg-grid__loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
}
.mg-grid__card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-left: 3px solid var(--m-border-strong);
border-radius: 10px;
color: var(--m-text);
text-align: left;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease, border-color 140ms ease, transform 140ms ease;
}
.mg-grid__card:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
transform: translateY(-1px);
}
.mg-grid__card:focus-visible {
outline: 2px solid var(--p-primary-color);
outline-offset: 2px;
}
.mg-grid__card.is-system { border-left-color: rgb(37, 99, 235); cursor: default; }
.mg-grid__card.is-system:hover { transform: none; }
.mg-grid__card.is-em-uso { border-left-color: rgb(22, 163, 74); }
.mg-grid__card.is-vazio { border-left-color: var(--m-border-strong); }
.mg-grid__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.mg-grid__dot {
width: 28px;
height: 28px;
border-radius: 50%;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.15);
}
.mg-grid__name {
font-size: 0.95rem;
font-weight: 600;
color: var(--m-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mg-grid__meta {
margin-top: auto;
padding-top: 4px;
}
.mg-grid__footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
border-top: 1px solid var(--m-border);
padding-top: 8px;
margin-top: 4px;
min-height: 36px;
}
/* Paginator standalone (grid view) */
.mg-paginator.p-paginator {
background: var(--m-bg-medium);
border: none;
border-top: 1px solid var(--m-border);
padding: 8px 12px;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
flex-shrink: 0;
}
.mg-paginator.p-paginator .p-paginator-current {
color: var(--m-text-muted);
font-size: 0.78rem;
background: transparent;
border: none;
padding: 0 6px;
}
.mg-paginator.p-paginator .p-paginator-first,
.mg-paginator.p-paginator .p-paginator-prev,
.mg-paginator.p-paginator .p-paginator-next,
.mg-paginator.p-paginator .p-paginator-last,
.mg-paginator.p-paginator .p-paginator-page {
min-width: 30px;
height: 30px;
color: var(--m-text);
background: transparent;
border: 1px solid var(--m-border);
border-radius: 8px;
font-size: 0.8rem;
transition: background-color 140ms ease, border-color 140ms ease;
}
.mg-paginator.p-paginator .p-paginator-page.p-paginator-page-selected {
background: var(--m-accent-soft);
border-color: var(--m-accent-strong);
color: var(--m-accent);
}
.mg-paginator.p-paginator .p-paginator-first:not(.p-disabled):hover,
.mg-paginator.p-paginator .p-paginator-prev:not(.p-disabled):hover,
.mg-paginator.p-paginator .p-paginator-next:not(.p-disabled):hover,
.mg-paginator.p-paginator .p-paginator-last:not(.p-disabled):hover,
.mg-paginator.p-paginator .p-paginator-page:not(.p-paginator-page-selected):hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mg-paginator.p-paginator .p-select {
background: transparent;
border: 1px solid var(--m-border);
border-radius: 8px;
height: 30px;
font-size: 0.78rem;
color: var(--m-text);
display: inline-flex;
align-items: center;
}
.mg-paginator.p-paginator .p-select-label {
padding: 0 8px;
color: var(--m-text);
font-size: 0.78rem;
display: flex;
align-items: center;
line-height: 1;
height: 100%;
background: transparent;
}
/* ─── Empty state ─── */
.mg-empty {
margin: 24px 0;
padding: 56px 28px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
color: var(--m-text-muted);
border: 2px dashed var(--m-border-strong);
border-radius: 12px;
background: color-mix(in srgb, var(--m-bg-soft) 40%, transparent);
gap: 8px;
}
.mg-empty__icon { font-size: 2rem; color: var(--m-text-faint); margin-bottom: 4px; }
.mg-empty__title { font-size: 0.92rem; font-weight: 600; color: var(--m-text); }
.mg-empty__hint { font-size: 0.78rem; }
.mg-empty__btn { margin-top: 8px; }
/* ─── Dialog "Pacientes do grupo" ─── */
.mg-pdlg__header {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.mg-pdlg__avatar {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 8px;
color: white;
font-weight: 700;
font-size: 0.95rem;
flex-shrink: 0;
overflow: hidden;
}
.mg-pdlg__avatar--sm {
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 0.78rem;
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
}
.mg-pdlg__avatar img { width: 100%; height: 100%; object-fit: cover; }
.mg-pdlg__title {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.mg-pdlg__name {
font-size: 1rem;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mg-pdlg__sub {
font-size: 0.72rem;
color: var(--m-text-muted);
}
.mg-pdlg__body {
display: flex;
flex-direction: column;
gap: 14px;
}
.mg-pdlg__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
}
.mg-pdlg__search {
flex: 1;
min-width: 200px;
max-width: 320px;
}
.mg-pdlg__count-pill {
padding: 4px 10px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
flex-shrink: 0;
}
.mg-pdlg__loading {
display: flex;
align-items: center;
gap: 8px;
padding: 20px;
font-size: 0.85rem;
justify-content: center;
}
.mg-pdlg__error {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: rgba(220, 38, 38, 0.10);
border: 1px solid rgba(220, 38, 38, 0.30);
border-radius: 8px;
color: rgb(220, 38, 38);
font-size: 0.82rem;
}
.mg-pdlg__empty {
padding: 36px 20px;
text-align: center;
color: var(--m-text-muted);
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.mg-pdlg__empty-icon {
width: 48px;
height: 48px;
display: grid;
place-items: center;
border-radius: 10px;
margin-bottom: 8px;
font-size: 1.4rem;
}
.mg-pdlg__empty-icon-pi {
font-size: 1.6rem;
opacity: 0.4;
margin-bottom: 8px;
}
.mg-pdlg__empty-title {
font-size: 0.92rem;
font-weight: 600;
color: var(--m-text);
}
.mg-pdlg__empty-hint {
font-size: 0.78rem;
}
.mg-pdlg__inline-clear-btn {
margin-top: 10px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border: 1px solid var(--m-border);
color: var(--m-text-muted);
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
}
.mg-pdlg__inline-clear-btn:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
color: var(--m-text);
}
/* DataTable interna do dialog — estilo mais leve que a tabela principal
pra não competir com o chrome do dialog. */
.mg-pdlg__table :deep(.p-datatable) {
background: transparent;
border: 1px solid var(--m-border);
border-radius: 10px;
overflow: hidden;
}
.mg-pdlg__table :deep(.p-datatable-thead > tr > th) {
background: var(--m-bg-soft) !important;
color: var(--m-text);
font-size: 0.74rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 10px 12px;
border-bottom: 1px solid var(--m-border);
}
.mg-pdlg__table :deep(.p-datatable-tbody > tr > td) {
padding: 10px 12px;
border-bottom: 1px solid var(--m-border);
background: transparent;
vertical-align: middle;
}
.mg-pdlg__table :deep(.p-datatable-tbody > tr:hover) {
background: var(--m-bg-soft-hover);
}
.mg-pdlg__table :deep(.p-paginator) {
background: var(--m-bg-soft);
border: none;
border-top: 1px solid var(--m-border);
padding: 8px 12px;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
}
.mg-pdlg__patient {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.mg-pdlg__patient-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.mg-pdlg__patient-name {
font-size: 0.88rem;
font-weight: 600;
color: var(--m-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mg-pdlg__patient-email {
font-size: 0.74rem;
color: var(--m-text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mg-pdlg__phone {
color: var(--m-text-muted);
font-size: 0.85rem;
}
.mg-pdlg__open-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border: 1px solid;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
transition: background-color 140ms ease;
}
.mg-pdlg__open-btn:hover {
background: color-mix(in srgb, currentColor 12%, transparent);
}
.mg-pdlg__open-btn > i { font-size: 0.7rem; }
/* ─── Drawer mobile ─── */
.mg-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;
}
.mg-mobile-drawer.is-open { transform: translateX(0); }
.mg-mobile-drawer__scroll {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mg-mobile-drawer__scroll .mg-side {
flex: 1;
min-height: 0;
width: 100%;
overflow: hidden;
background: transparent;
border-right: none;
display: flex;
flex-direction: column;
}
.mg-mobile-drawer__scroll .mg-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;
}
.mg-mobile-drawer__scroll .mg-side__scroll::-webkit-scrollbar { width: 5px; }
.mg-mobile-drawer__scroll .mg-side__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mg-mobile-drawer__scroll .mg-w--side {
margin: 0;
flex-shrink: 0;
}
.mg-mobile-drawer__scroll .mg-w--side:last-of-type { margin-bottom: 0; }
.mg-mobile-drawer__scroll .mg-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%);
}
.mg-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;
}
.mg-drawer-fade-enter-active,
.mg-drawer-fade-leave-active { transition: opacity 200ms ease; }
.mg-drawer-fade-enter-from,
.mg-drawer-fade-leave-to { opacity: 0; }
/* Botão X de fechar no header do dialog "Pacientes do grupo" estilo
igual ao .mg-close da página (32×32, bg --m-bg-soft, border). Usa
:global() porque o Dialog do PrimeVue é teleportado pra body, fora
do escopo scoped do componente. */
:global(.mg-pdlg-close-btn) {
width: 32px !important;
height: 32px !important;
background: var(--m-bg-soft) !important;
border: 1px solid var(--m-border) !important;
color: var(--m-text) !important;
border-radius: 9px !important;
transition: background-color 140ms ease !important;
}
:global(.mg-pdlg-close-btn:hover) {
background: var(--m-bg-soft-hover) !important;
}
:global(.mg-pdlg-close-btn .p-button-icon) {
font-size: 0.85rem !important;
}
/* ─── Color preset (no dialog de criar/editar grupo) ─── */
.mg-color-preset {
width: 28px;
height: 28px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.18);
cursor: pointer;
transition: transform 140ms ease, box-shadow 140ms ease;
}
.mg-color-preset:hover {
transform: scale(1.12);
}
.mg-color-preset.is-selected {
box-shadow: 0 0 0 2px var(--p-content-background), 0 0 0 4px var(--p-primary-color);
transform: scale(1.08);
}
/* ─── Mobile (<1024px) ─── */
@media (max-width: 1023px) {
.mg-body { flex-direction: column; padding: 0; }
.mg-main { width: 100%; padding: 8px; }
.mg-page__title > span:first-of-type { display: none; }
.mg-page__title-icon { display: none; }
.mg-menu-btn--mobile-only { display: inline-flex; }
.mg-act-btn--primary span { display: none; }
.mg-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
}
</style>