From 4fc0e3a02b96802e51ac7d2c54c025fcf36a0b2d Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 8 May 2026 09:43:03 -0300 Subject: [PATCH] MelissaPaciente Fase 3: Tab Perfil completa (6 sections stacked + anchors) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EXTENSAO: src/features/patients/utils/patientFormatters.js - +5 formatters: pickField (compartilhado), onlyDigits, fmtCPF (000.000.000-00), fmtRG (passthrough), fmtPhoneMobile ((XX) 9XXXX-XXXX), fmtGender (Masculino/Feminino/Nao-binario/Outro), fmtMarital (Solteiro/Casado/ Divorciado/Viuvo/Uniao estavel). MELISSAPACIENTE.VUE — script - 30+ field computeds usando pickField (cobre snake_case + camelCase): birthValue, telefone/Alternativo, email/Alternativo, genero, estadoCivil, naturalidade, ondeNosConheceu, encaminhadoPor, observacoes, notasInternas + 8 campos de endereco + 5 dados adicionais + 4 responsavel. - groupNames/groupLabel/groupCountLabel pra bloco Origem. - scrollToProfileSection(key): liga sidebar sub-nav -> scrollIntoView do anchor #mpa-perfil-XXX. Em mobile fecha o drawer. MELISSAPACIENTE.VUE — Tab Perfil reescrita Diferente do PatientProntuario legacy que usa PrimeVue Accordion (1 painel aberto por vez), o Melissa nativo mostra os 6 cards stacked com scroll suave do sidebar sub-nav. Mais legivel em desktop, mais rapido de escanear. - 1. Informacoes Pessoais: 2-col com Dados de cadastro (nome/data nasc com idade inline/genero/estado civil/CPF/RG/naturalidade) + Contato + Origem (grupos/tags chips/onde nos conheceu/encaminhado por). tel: e mailto: links onde ha valor. Observacoes full-width quando preenchido. - 2. Endereco: grid 2-col com 8 fields. - 3. Dados Adicionais: grid 2-col com escolaridade/profissao/parente/grau/ tel parente. - 4. Responsavel: 1-col com nome/CPF/tel + observacao block textual. - 5. Anotacoes Internas: card com hint lock + textblock min-height. - 6. Sessoes: lista compacta scrollable (max-height 360px) com titulo/ data/duracao/modalidade chips + tag status. CSS: ~250L novos pros componentes (mpa-fields/field-row/field-grid-2/ field-block/sess/sess-list). Pattern visual Melissa: cards com label uppercase, separadores horizontais sutis, links primary, monospace pra CPF/RG/CEP. ESLint: 0 errors da minha mudanca. Co-Authored-By: Claude Opus 4.7 (1M context) --- Obsidian/Brain/log.md | 47 ++ .../patients/utils/patientFormatters.js | 76 +++ src/layout/melissa/MelissaPaciente.vue | 590 +++++++++++++++++- 3 files changed, 691 insertions(+), 22 deletions(-) diff --git a/Obsidian/Brain/log.md b/Obsidian/Brain/log.md index 0164ec5..a9b3eca 100644 --- a/Obsidian/Brain/log.md +++ b/Obsidian/Brain/log.md @@ -50,6 +50,53 @@ Touched: none ## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB Touched: none +## [2026-05-08 14:30] session | MelissaPaciente Fase 3 — Tab Perfil (6 sections stacked) +Touched: none +Detalhes: Substituiu o placeholder da aba Perfil por 6 sections stacked +com anchors no MelissaPaciente. Diferente do PatientProntuario legacy que +usava PrimeVue Accordion (1 painel aberto por vez), o Melissa nativo +mostra todos os 6 cards stacked com scroll suave do sidebar sub-nav pra +cada anchor. Mais legivel em desktop, mais rapido pra escanear. + +EXTENSAO de patientFormatters.js: +5 formatters +- pickField (ja existia computed local; agora helper compartilhavel) +- onlyDigits, fmtCPF (000.000.000-00), fmtRG (passthrough), fmtPhoneMobile + ((XX) 9XXXX-XXXX), fmtGender (Masculino/Feminino/Nao-binario/Outro), + fmtMarital (Solteiro/Casado/Divorciado/Viuvo/Uniao estavel). + +MELISSAPACIENTE.VUE — script: +- 30+ field computeds usando pickField (cobre snake_case + camelCase do + schema): birthValue, telefone/Alternativo, email/Alternativo, genero, + estadoCivil, naturalidade, ondeNosConheceu, encaminhadoPor, observacoes, + notasInternas + 8 campos de endereco (cep/pais/cidade/estado/endereco/ + numero/bairro/complemento) + 5 dados adicionais (escolaridade/profissao/ + nomeParente/grauParentesco/telefoneParente) + 4 responsavel. +- groupNames/groupLabel/groupCountLabel pra Origem. +- scrollToProfileSection(key) que liga sidebar sub-nav -> nextTick -> + scrollIntoView do anchor #mpa-perfil-XXX. Em mobile fecha o drawer. + +MELISSAPACIENTE.VUE — template Tab Perfil: +- 1. Informacoes Pessoais: 2-col (Dados de cadastro: nome/data nasc com + idade/genero/estado civil/CPF/RG/naturalidade) + (Contato: tel/tel-alt + com tel: links + e-mail principal/alt com mailto: + Origem: grupos/tags + chips/onde nos conheceu/encaminhado por). Observacoes full-width quando + preenchido. +- 2. Endereco: grid 2-col com 8 fields (CEP/pais/cidade/estado/endereco/ + numero/bairro/complemento). +- 3. Dados Adicionais: grid 2-col com escolaridade/profissao/parente/grau/ + tel parente. +- 4. Responsavel: 1-col com nome/CPF/tel + observacao block textual. +- 5. Anotacoes Internas: card com hint lock + textblock min-height. +- 6. Sessoes: lista compacta scrollable (max-height 360px) com titulo/ + data/duracao/modalidade chips + tag status. + +CSS: ~250L novos pros componentes (mpa-fields/field-row/field-grid-2/ +field-block/sess/sess-list). Pattern visual Melissa: cards com label +uppercase, separadores horizontais sutis, links com cor primary, monospace +pra CPF/RG/CEP. + +ESLint: 0 errors da minha mudanca. + ## [2026-05-08 13:00] session | MelissaPaciente Fase 2 — Tab Visao Geral completa Touched: none Detalhes: Reescreveu a aba Visao Geral do MelissaPaciente substituindo o diff --git a/src/features/patients/utils/patientFormatters.js b/src/features/patients/utils/patientFormatters.js index 219285b..2320756 100644 --- a/src/features/patients/utils/patientFormatters.js +++ b/src/features/patients/utils/patientFormatters.js @@ -40,6 +40,82 @@ export function dash(v) { return s || '—'; } +/** + * Pega a primeira chave nao vazia de um objeto (snake_case ou camelCase). + * Usado pra resolver discrepancias de schema (ex: 'data_nascimento' vs 'birth_date'). + */ +export function pickField(obj, keys = []) { + for (const k of keys) { + const v = obj?.[k]; + if (v !== null && v !== undefined && String(v).trim()) return v; + } + return null; +} + +export function onlyDigits(v) { + return String(v ?? '').replace(/\D/g, ''); +} + +/** + * Formata CPF: 00000000000 -> 000.000.000-00 + */ +export function fmtCPF(v) { + const d = onlyDigits(v); + if (!d) return '—'; + if (d.length !== 11) return d; + return `${d.slice(0, 3)}.${d.slice(3, 6)}.${d.slice(6, 9)}-${d.slice(9)}`; +} + +/** + * Formata RG (genericamente — varia por estado): + * 00.000.000-0 / 0000000000 → mantem digitos com pontos a cada 3 a partir da direita. + */ +export function fmtRG(v) { + const s = String(v ?? '').trim(); + if (!s) return '—'; + return s; +} + +/** + * Formata telefone celular pt-br: (XX) 9XXXX-XXXX ou (XX) XXXX-XXXX. + */ +export function fmtPhoneMobile(v) { + const d = onlyDigits(v); + if (!d) return '—'; + if (d.length === 11) return `(${d.slice(0, 2)}) ${d.slice(2, 7)}-${d.slice(7, 11)}`; + if (d.length === 10) return `(${d.slice(0, 2)}) ${d.slice(2, 6)}-${d.slice(6, 10)}`; + return d; +} + +/** + * Mapeia variantes de genero pra label legivel. + */ +export function fmtGender(v) { + const s = String(v ?? '').trim(); + if (!s) return '—'; + const x = s.toLowerCase(); + if (['m', 'masc', 'masculino', 'male', 'man', 'homem'].includes(x)) return 'Masculino'; + if (['f', 'fem', 'feminino', 'female', 'woman', 'mulher'].includes(x)) return 'Feminino'; + if (['nb', 'nao-binario', 'não-binário', 'nonbinary', 'non-binary'].includes(x)) return 'Não-binário'; + if (['outro', 'other'].includes(x)) return 'Outro'; + return s; +} + +/** + * Mapeia variantes de estado civil pra label pt-br. + */ +export function fmtMarital(v) { + const s = String(v ?? '').trim(); + if (!s) return '—'; + const x = s.toLowerCase(); + if (['solteiro', 'solteira', 'single'].includes(x)) return 'Solteiro(a)'; + if (['casado', 'casada', 'married'].includes(x)) return 'Casado(a)'; + if (['divorciado', 'divorciada', 'divorced'].includes(x)) return 'Divorciado(a)'; + if (['viuvo', 'viúva', 'viuvo(a)', 'widowed'].includes(x)) return 'Viúvo(a)'; + if (['uniao estavel', 'união estável', 'civil union'].includes(x)) return 'União estável'; + return s; +} + export function fmtDateBR(v) { const d = parseDateLoose(v); if (!d) return v ? dash(v) : '—'; diff --git a/src/layout/melissa/MelissaPaciente.vue b/src/layout/melissa/MelissaPaciente.vue index 34b529c..0bc2417 100644 --- a/src/layout/melissa/MelissaPaciente.vue +++ b/src/layout/melissa/MelissaPaciente.vue @@ -18,7 +18,7 @@ * * Prefixo CSS: .mpa-* (Melissa PAciente). */ -import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'; +import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'; import { useToast } from 'primevue/usetoast'; import MelissaConfigList from './MelissaConfigList.vue'; import { usePatientDetail } from '@/features/patients/composables/usePatientDetail'; @@ -27,11 +27,17 @@ import { usePatientFinancial } from '@/features/patients/composables/usePatientF import { usePatientMessages } from '@/features/patients/composables/usePatientMessages'; import { usePatientDocuments } from '@/features/patients/composables/usePatientDocuments'; import { + pickField, calcAge, fmtRelative, fmtDateBR, fmtDateTimeBR, fmtCurrency, + fmtCPF, + fmtRG, + fmtGender, + fmtMarital, + fmtPhoneMobile, sessionDuration, STATUS_LABEL, STATUS_SEVERITY, @@ -95,6 +101,16 @@ const PROFILE_SECTIONS = [ { key: 'sess', label: 'Sessões', icon: 'pi pi-calendar' } ]; const activeProfileSection = ref('pessoais'); +function scrollToProfileSection(key) { + activeProfileSection.value = key; + if (isMobile.value) drawerOpen.value = false; + nextTick(() => { + const el = document.getElementById(`mpa-perfil-${key}`); + if (el && typeof el.scrollIntoView === 'function') { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); +} // ── Helpers de exibicao ──────────────────────────────────── function dash(v) { @@ -127,16 +143,47 @@ function tagStyle(t) { return tagStyleHelper(t); } -// ── Notas + observacoes (campos opcionais do paciente) ───── -function pickField(obj, keys = []) { - for (const k of keys) { - const v = obj?.[k]; - if (v !== null && v !== undefined && String(v).trim()) return v; - } - return null; -} -const observacoes = computed(() => pickField(patientData.value, ['observacoes', 'notes_short'])); -const notasInternas = computed(() => pickField(patientData.value, ['notas_internas', 'notes'])); +// ── Field computeds (cobre snake_case e camelCase do schema) ─ +const birthValue = computed(() => pickField(patientData.value, ['data_nascimento', 'birth_date'])); +const telefone = computed(() => pickField(patientData.value, ['telefone', 'phone'])); +const telefoneAlternativo = computed(() => pickField(patientData.value, ['telefone_alternativo', 'phone_alt', 'phoneAlt'])); +const emailPrincipal = computed(() => pickField(patientData.value, ['email_principal', 'email'])); +const emailAlternativo = computed(() => pickField(patientData.value, ['email_alternativo', 'email_alt', 'emailAlt'])); +const genero = computed(() => pickField(patientData.value, ['genero', 'gender'])); +const estadoCivil = computed(() => pickField(patientData.value, ['estado_civil', 'marital_status'])); +const naturalidade = computed(() => pickField(patientData.value, ['naturalidade', 'birthplace', 'place_of_birth'])); +const ondeNosConheceu = computed(() => pickField(patientData.value, ['onde_nos_conheceu', 'lead_source'])); +const encaminhadoPor = computed(() => pickField(patientData.value, ['encaminhado_por', 'referred_by'])); +const observacoes = computed(() => pickField(patientData.value, ['observacoes', 'notes_short'])); +const notasInternas = computed(() => pickField(patientData.value, ['notas_internas', 'notes'])); + +// Endereço +const cep = computed(() => pickField(patientData.value, ['cep', 'postal_code'])); +const pais = computed(() => pickField(patientData.value, ['pais', 'country'])); +const cidade = computed(() => pickField(patientData.value, ['cidade', 'city'])); +const estado = computed(() => pickField(patientData.value, ['estado', 'state'])); +const endereco = computed(() => pickField(patientData.value, ['endereco', 'address_line'])); +const numero = computed(() => pickField(patientData.value, ['numero', 'address_number'])); +const bairro = computed(() => pickField(patientData.value, ['bairro', 'neighborhood'])); +const complemento = computed(() => pickField(patientData.value, ['complemento', 'address_complement'])); + +// Dados Adicionais +const escolaridade = computed(() => pickField(patientData.value, ['escolaridade', 'education', 'education_level'])); +const profissao = computed(() => pickField(patientData.value, ['profissao', 'profession'])); +const nomeParente = computed(() => pickField(patientData.value, ['nome_parente', 'relative_name'])); +const grauParentesco = computed(() => pickField(patientData.value, ['grau_parentesco', 'relative_relation'])); +const telefoneParente = computed(() => pickField(patientData.value, ['telefone_parente', 'relative_phone'])); + +// Responsável (pra menores) +const nomeResponsavel = computed(() => pickField(patientData.value, ['nome_responsavel', 'guardian_name'])); +const cpfResponsavel = computed(() => pickField(patientData.value, ['cpf_responsavel', 'guardian_cpf'])); +const telefoneResponsavel = computed(() => pickField(patientData.value, ['telefone_responsavel', 'guardian_phone'])); +const observacaoResponsavel = computed(() => pickField(patientData.value, ['observacao_responsavel', 'guardian_note'])); + +// Grupos como string label (Origem na Informacoes Pessoais) +const groupNames = computed(() => detail.groups.value.map((g) => g?.name).filter(Boolean)); +const groupLabel = computed(() => groupNames.value.length ? groupNames.value.join(', ') : '—'); +const groupCountLabel = computed(() => groupNames.value.length <= 1 ? 'Grupo' : 'Grupos'); // ── KPIs Visao Geral (Fase 2) ────────────────────────────── const kpiSessoes = computed(() => sessionsHook.totalSessoes.value); @@ -349,7 +396,7 @@ void toast; type="button" class="mpa-tab-btn mpa-tab-btn--sub" :class="{ 'is-active': activeProfileSection === s.key }" - @click="activeProfileSection = s.key" + @click="scrollToProfileSection(s.key)" > {{ s.label }} @@ -625,23 +672,323 @@ void toast; - +
-
+ +
-
+
-
Perfil — Fase 3
-
Accordion 6 seções (pessoais, endereço, dados, responsável, anotações, sessões)
+
1. Informações Pessoais
+
Cadastro, contato e origem
-

- Em desenvolvimento — Fase 3. Seção atual: - {{ activeProfileSection }}. -

+
+ +
+

Dados de cadastro

+
+ Nome completo + {{ dash(nomeCompleto) }} +
+
+ Data de nascimento + + {{ fmtDateBR(birthValue) }} + + ({{ calcAge(birthValue) }} a) + + +
+
+ Gênero + {{ fmtGender(genero) }} +
+
+ Estado civil + {{ fmtMarital(estadoCivil) }} +
+
+ CPF + {{ fmtCPF(patientData.cpf) }} +
+
+ RG + {{ fmtRG(patientData.rg) }} +
+
+ Naturalidade + {{ dash(naturalidade) }} +
+
+ + +
+
+

Contato

+
+ Telefone / Celular + + {{ fmtPhoneMobile(telefone) }} + + +
+
+ Telefone alternativo + + {{ fmtPhoneMobile(telefoneAlternativo) }} + + +
+
+ E-mail principal + + {{ emailPrincipal }} + + +
+
+ E-mail alternativo + + {{ emailAlternativo }} + + +
+
+ +
+

Origem

+
+ {{ groupCountLabel }} + {{ groupLabel }} +
+
+ Tags +
+ + {{ t.name }} + + +
+
+
+ Onde nos conheceu? + {{ dash(ondeNosConheceu) }} +
+
+ Encaminhado por + {{ dash(encaminhadoPor) }} +
+
+
+
+ + +
+

Observações

+

{{ observacoes }}

+
-
+ + + +
+
+
+
+
2. Endereço
+
Localização do paciente
+
+
+
+
+

Localização

+
+
+ CEP + {{ dash(cep) }} +
+
+ País + {{ dash(pais) }} +
+
+ Cidade + {{ dash(cidade) }} +
+
+ Estado + {{ dash(estado) }} +
+
+ Endereço + {{ dash(endereco) }} +
+
+ Número + {{ dash(numero) }} +
+
+ Bairro + {{ dash(bairro) }} +
+
+ Complemento + {{ dash(complemento) }} +
+
+
+
+
+ + +
+
+
+
+
3. Dados Adicionais
+
Formação e contato familiar
+
+
+
+
+

Formação & família

+
+
+ Escolaridade + {{ dash(escolaridade) }} +
+
+ Profissão + {{ dash(profissao) }} +
+
+ Nome de um parente + {{ dash(nomeParente) }} +
+
+ Grau de parentesco + {{ dash(grauParentesco) }} +
+
+ Telefone do parente + + {{ fmtPhoneMobile(telefoneParente) }} + + +
+
+
+
+
+ + +
+
+
+
+
4. Responsável
+
Para pacientes menores de idade
+
+
+
+
+

Dados do responsável

+
+ Nome + {{ dash(nomeResponsavel) }} +
+
+ CPF + {{ fmtCPF(cpfResponsavel) }} +
+ +
+

Observação

+

{{ observacaoResponsavel }}

+
+
+
+
+ + +
+
+
+
+
5. Anotações Internas
+
Visível só pra você
+
+
+
+
+
+ + Campo interno — não aparece no cadastro externo. +
+

+ {{ notasInternas ? notasInternas : '—' }} +

+
+
+
+ + +
+
+
+
+
6. Sessões
+
{{ kpiSessoes }} no histórico — abertura completa na aba Agenda
+
+
+
+
+ Carregando sessões… +
+
+
+
Nenhuma sessão
+
As sessões agendadas com este paciente aparecerão aqui.
+
+
+
+
+

+ {{ s.titulo_custom || s.titulo || (s.tipo ? s.tipo : 'Sessão') }} +

+
+ {{ fmtDateTimeBR(s.inicio_em) }} + + {{ sessionDuration(s.inicio_em, s.fim_em) }} + + + + {{ s.modalidade === 'online' ? 'Online' : 'Presencial' }} + +
+

{{ s.observacoes }}

+
+ +
+
+
+
@@ -1524,6 +1871,205 @@ void toast; -webkit-box-orient: vertical; } +/* ═══════ Tab Perfil — fields stacked grid ═══════ */ +.mpa-fields-grid { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} +@media (min-width: 720px) { + .mpa-fields-grid { + grid-template-columns: 1fr 1fr; + } +} +.mpa-fields-stack { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; +} + +.mpa-fields { + border-radius: 10px; + border: 1px solid var(--m-border); + background: var(--m-bg-medium); + padding: 12px 14px; +} +.mpa-fields--full { + margin-top: 12px; + grid-column: 1 / -1; +} +.mpa-fields__label { + font-size: 0.66rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--m-text-muted); + margin-bottom: 8px; + opacity: 0.85; +} +.mpa-fields__textblock { + font-size: 0.85rem; + color: var(--m-text); + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; +} +.mpa-fields__textblock--min { min-height: 3.5rem; } +.mpa-fields__lock-hint { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.7rem; + color: var(--m-text-muted); + opacity: 0.75; + margin-bottom: 10px; +} +.mpa-fields__lock-hint > i { font-size: 0.66rem; } + +/* Field row generico (k = label esq, v = valor dir) */ +.mpa-field-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + padding: 6px 0; + border-bottom: 1px solid var(--m-border); + font-size: 0.82rem; +} +.mpa-field-row--last { border-bottom: none; } +.mpa-field-row--align-start { align-items: flex-start; } +.mpa-field-row__k { + color: var(--m-text-muted); + flex-shrink: 0; +} +.mpa-field-row__v { + color: var(--m-text); + font-weight: 600; + text-align: right; + word-break: break-word; +} +.mpa-field-row__v--mono { + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.78rem; +} +.mpa-field-row__v--link { + color: var(--p-primary-color); + text-decoration: none; +} +.mpa-field-row__v--link:hover { text-decoration: underline; } +.mpa-field-row__v--truncate { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; +} +.mpa-field-row__v-dim { + color: var(--m-text-muted); + font-weight: 400; + margin-left: 4px; +} +.mpa-field-row__chips { + display: flex; + flex-wrap: wrap; + gap: 4px; + justify-content: flex-end; +} + +/* Grid 2-col pra blocos de endereco/dados (8 fields em 2 colunas) */ +.mpa-field-grid-2 { + display: grid; + grid-template-columns: 1fr; +} +@media (min-width: 600px) { + .mpa-field-grid-2 { + grid-template-columns: 1fr 1fr; + column-gap: 18px; + } + .mpa-field-grid-2 > .mpa-field-row { + border-bottom: 1px solid var(--m-border); + } + /* Ultima da esq tira borda quando ultima do grid; --last e atribuida no template */ + .mpa-field-grid-2 > .mpa-field-row--last { border-bottom: none; } +} + +/* Bloco textual full-width dentro de .mpa-fields (ex: Observacao Responsavel) */ +.mpa-field-block { + padding: 10px 0; + border-top: 1px solid var(--m-border); +} +.mpa-field-block--last { border-bottom: none; } +.mpa-field-block__label { + font-size: 0.7rem; + font-weight: 700; + color: var(--m-text-muted); + margin-bottom: 4px; +} +.mpa-field-block__text { + font-size: 0.82rem; + color: var(--m-text); + line-height: 1.5; + white-space: pre-wrap; +} + +/* Mini lista de sessoes (panel 6 da aba Perfil) */ +.mpa-sess-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 360px; + overflow-y: auto; + padding-right: 2px; +} +.mpa-sess { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--m-border); + background: var(--m-bg-medium); +} +.mpa-sess__main { + flex: 1; + min-width: 0; +} +.mpa-sess__title { + font-size: 0.85rem; + font-weight: 700; + color: var(--m-text); + line-height: 1.2; + margin-bottom: 4px; +} +.mpa-sess__meta { + display: flex; + flex-wrap: wrap; + gap: 4px 10px; + font-size: 0.72rem; + color: var(--m-text-muted); +} +.mpa-sess__meta > span { + display: inline-flex; + align-items: center; + gap: 4px; +} +.mpa-sess__meta > span > i { font-size: 0.66rem; opacity: 0.6; } +.mpa-sess__obs { + font-size: 0.74rem; + color: var(--m-text-muted); + line-height: 1.4; + margin-top: 4px; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} +.mpa-sess__tag { + flex-shrink: 0; + font-size: 0.66rem !important; +} + /* ═══════ Notas e observacoes ═══════ */ .mpa-notes { display: flex;