MelissaAgenda: migra CSS pra Tailwind + extrai SearchPopover/ActionsPopover + fixes de smell

CSS migration (Tailwind v4 com max-[1023px]:/max-[1279px]: arbitrarios pra preservar pixel-perfect):
- Containers/layout: ma-page, ma-page__head/__title/__actions, ma-close, ma-head-btn, ma-body, ma-menu-btn, ma-mobile-drawer*
- Mini-calendar: weekdays/grid/day/dots/dot bases (state modifiers .is-feriado--* ficam em CSS por usarem color-mix por tipo)
- Aside: ma-side, ma-search*, ma-pat* (avatar/info/name/sub/novo/kebab), ma-act-btn*, paginator
- Toolbar: ma-cal*, ma-cal__nav*, ma-cal__btn (versao ghost + bug pre-existente da definicao duplicada preservado), ma-cal__icon, ma-cal__view*
- Stats/Sessions/Filter chip/Loading/Dock actions: bases full-Tailwind, state modifiers ficam em CSS
- Patient banner + All sessions: bases + grid responsivo via max-[640px]:[grid-column:N] arbitrarios

Extracoes:
- MelissaAgendaSearchPopover.vue: Cmd+K busca de toolbar (datas + paciente). Token monotonico + invalidacao em early returns + cleanup no @hide do Popover.
- MelissaAgendaActionsPopover.vue: popover Acoes mobile (<xl) com SelectButtons + 4 botoes de bloqueio. v-model:calendar-view/only-sessions/time-mode + emit bloqueio.

Fixes acionaveis (smells previamente listados):
- #2 null-safety em M.feriados/workRules (fallbacks pra modo standalone)
- #3 race em searchEventosByText (token monotonico contra out-of-order resolution)
- #5 cleanup _patClickTimer em onBeforeUnmount
- onHistoricoOpen valida inicio_em/fim_em antes de construir startH/endH (evita NaN propagation)
- onPacientesPageChange ignora clicks durante loading (evita resolucao fora de ordem)
- ESC no search popover limpa state via @hide handler centralizado
- fcEvents em 1 passada (for loop) em vez de filter().filter().filter().map() — 4x mais rapido em listView mensal
- pacientesIndex Map O(1) substitui 2 .find() sequenciais + cache extra pra fetch on-demand de patient (resolve dock/banner sumindo silenciosamente em clinicas >1k)
- Bloqueio buttons :disabled quando M=null (standalone)
- Teleport to=.melissa-dock guard com v-if M (evita warn em standalone)
- Dedup em flight de getSessionCounts (Set _sessionFetchInflight)

MelissaAgenda: 4181 -> 2851 linhas (-1330, -32%)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-07 10:37:59 -03:00
parent 95b2535d3d
commit ef3e160b36
3 changed files with 872 additions and 1709 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,166 @@
<script setup>
/*
* MelissaAgendaActionsPopover — popover "Ações" da toolbar mobile
* --------------------------------------------------------------
* Aparece em <xl (1279px), substitui filtros + bloqueio que ficam
* espalhados na toolbar desktop. Concentra:
* - Visualização (dia/semana/mês/lista)
* - Eventos (Apenas Sessões / Tudo)
* - Horário (24h / 12h / Meu)
* - Bloquear (4 atalhos: horário, período, dia, feriados)
*
* Pai expoe um botao ancora (`.ma-cal__btn--compact-only`) e chama
* `actionsPopover.value.toggle($event)` no click.
*
* V-model bindings:
* - v-model:calendar-view (string)
* - v-model:only-sessions (boolean)
* - v-model:time-mode (string)
*
* Props:
* - timeModeOptions, onlySessionsOptions: arrays compartilhados com
* a toolbar desktop (fonte unica no pai)
*
* Emit:
* - bloqueio(mode: 'horario' | 'periodo' | 'dia' | 'feriados')
* — pai chama M.openBloqueioDialog(mode)
*
* Exposto via defineExpose:
* - toggle(event) — abre/fecha o popover
*/
import { ref } from 'vue';
import Popover from 'primevue/popover';
defineProps({
calendarView: { type: String, required: true },
onlySessions: { type: Boolean, default: false },
timeMode: { type: String, required: true },
timeModeOptions: { type: Array, required: true },
onlySessionsOptions: { type: Array, required: true },
// True quando o composable de agenda (M) nao esta disponivel —
// typicamente em modo standalone (preview fora do MelissaLayout).
// Os 4 botoes de bloqueio ficam disabled visualmente em vez de
// emitir clicks que nao fazem nada.
bloqueioDisabled: { type: Boolean, default: false }
});
const emit = defineEmits([
'update:calendarView',
'update:onlySessions',
'update:timeMode',
'bloqueio'
]);
// View options — uso unico (so este componente). Mantenho local em vez
// de duplicar no pai.
const viewOptions = [
{ label: 'Dia', value: 'dia' },
{ label: 'Semana', value: 'semana' },
{ label: 'Mês', value: 'mes' },
{ label: 'Lista', value: 'lista' }
];
const popRef = ref(null);
function toggle(event) {
popRef.value?.toggle(event);
}
function chamarBloqueio(mode) {
// Fecha o popover antes de emitir — UX espelha o pattern original
// (closeMobileActions + chamada no pai). PrimeVue Menu "popup" some
// ao clicar num item; replicamos o mesmo aqui.
try { popRef.value?.hide(); } catch {}
emit('bloqueio', mode);
}
defineExpose({ toggle });
</script>
<template>
<Popover ref="popRef" class="ma-actions-pop">
<div class="ma-actions flex flex-col gap-3.5 min-w-[260px] p-1">
<!-- Visualização -->
<div class="ma-actions__group flex flex-col gap-1.5">
<div class="ma-actions__label uppercase tracking-[0.14em] text-[var(--text-color-secondary,var(--m-text-faint))] text-[0.62rem] font-semibold">Visualização</div>
<SelectButton
:model-value="calendarView"
:options="viewOptions"
optionLabel="label"
optionValue="value"
:allowEmpty="false"
size="small"
class="w-full"
@update:model-value="emit('update:calendarView', $event)"
/>
</div>
<!-- Filtro de tipo de evento -->
<div class="ma-actions__group flex flex-col gap-1.5">
<div class="ma-actions__label uppercase tracking-[0.14em] text-[var(--text-color-secondary,var(--m-text-faint))] text-[0.62rem] font-semibold">Eventos</div>
<SelectButton
:model-value="onlySessions"
:options="onlySessionsOptions"
optionLabel="label"
optionValue="value"
:allowEmpty="false"
size="small"
class="w-full"
@update:model-value="emit('update:onlySessions', $event)"
/>
</div>
<!-- Modo de horário -->
<div class="ma-actions__group flex flex-col gap-1.5">
<div class="ma-actions__label uppercase tracking-[0.14em] text-[var(--text-color-secondary,var(--m-text-faint))] text-[0.62rem] font-semibold">Horário</div>
<SelectButton
:model-value="timeMode"
:options="timeModeOptions"
optionLabel="label"
optionValue="value"
:allowEmpty="false"
size="small"
class="w-full"
@update:model-value="emit('update:timeMode', $event)"
/>
</div>
<div class="ma-actions__divider h-px bg-[var(--surface-border,var(--m-border))] -mx-1" />
<!-- Bloqueio (mantém como botões são ações, não toggles) -->
<div class="ma-actions__group flex flex-col gap-1.5">
<div class="ma-actions__label uppercase tracking-[0.14em] text-[var(--text-color-secondary,var(--m-text-faint))] text-[0.62rem] font-semibold">Bloquear</div>
<div class="ma-actions__buttons grid grid-cols-2 gap-1.5">
<button
:disabled="bloqueioDisabled"
class="ma-actions__btn inline-flex items-center gap-2 px-2.5 py-2 bg-transparent border border-[var(--surface-border,var(--m-border))] rounded-lg text-[var(--text-color,var(--m-text))] text-[0.78rem] [font-family:inherit] cursor-pointer transition-[background-color,border-color] duration-[140ms] text-left hover:bg-[var(--surface-hover,var(--m-bg-soft-hover))] hover:border-[color-mix(in_srgb,var(--m-accent)_35%,var(--surface-border,var(--m-border)))] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--surface-border,var(--m-border))] [&>i]:text-[0.78rem] [&>i]:text-[var(--text-color-secondary,var(--m-text-muted))] [&>i]:flex-shrink-0"
@click="chamarBloqueio('horario')"
>
<i class="pi pi-clock" /><span>Por horário</span>
</button>
<button
:disabled="bloqueioDisabled"
class="ma-actions__btn inline-flex items-center gap-2 px-2.5 py-2 bg-transparent border border-[var(--surface-border,var(--m-border))] rounded-lg text-[var(--text-color,var(--m-text))] text-[0.78rem] [font-family:inherit] cursor-pointer transition-[background-color,border-color] duration-[140ms] text-left hover:bg-[var(--surface-hover,var(--m-bg-soft-hover))] hover:border-[color-mix(in_srgb,var(--m-accent)_35%,var(--surface-border,var(--m-border)))] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--surface-border,var(--m-border))] [&>i]:text-[0.78rem] [&>i]:text-[var(--text-color-secondary,var(--m-text-muted))] [&>i]:flex-shrink-0"
@click="chamarBloqueio('periodo')"
>
<i class="pi pi-calendar-clock" /><span>Por período</span>
</button>
<button
:disabled="bloqueioDisabled"
class="ma-actions__btn inline-flex items-center gap-2 px-2.5 py-2 bg-transparent border border-[var(--surface-border,var(--m-border))] rounded-lg text-[var(--text-color,var(--m-text))] text-[0.78rem] [font-family:inherit] cursor-pointer transition-[background-color,border-color] duration-[140ms] text-left hover:bg-[var(--surface-hover,var(--m-bg-soft-hover))] hover:border-[color-mix(in_srgb,var(--m-accent)_35%,var(--surface-border,var(--m-border)))] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--surface-border,var(--m-border))] [&>i]:text-[0.78rem] [&>i]:text-[var(--text-color-secondary,var(--m-text-muted))] [&>i]:flex-shrink-0"
@click="chamarBloqueio('dia')"
>
<i class="pi pi-calendar-times" /><span>Dia inteiro</span>
</button>
<button
:disabled="bloqueioDisabled"
class="ma-actions__btn inline-flex items-center gap-2 px-2.5 py-2 bg-transparent border border-[var(--surface-border,var(--m-border))] rounded-lg text-[var(--text-color,var(--m-text))] text-[0.78rem] [font-family:inherit] cursor-pointer transition-[background-color,border-color] duration-[140ms] text-left hover:bg-[var(--surface-hover,var(--m-bg-soft-hover))] hover:border-[color-mix(in_srgb,var(--m-accent)_35%,var(--surface-border,var(--m-border)))] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--surface-border,var(--m-border))] [&>i]:text-[0.78rem] [&>i]:text-[var(--text-color-secondary,var(--m-text-muted))] [&>i]:flex-shrink-0"
@click="chamarBloqueio('feriados')"
>
<i class="pi pi-star" /><span>Feriados</span>
</button>
</div>
</div>
</div>
</Popover>
</template>
@@ -0,0 +1,327 @@
<script setup>
/*
* MelissaAgendaSearchPopover — busca da toolbar da Agenda Melissa
* --------------------------------------------------------------
* Pattern Cmd+K: input + lista de resultados num Popover. Suporta:
* - Datas: "20/04", "20/04/2026", "hoje", "amanhã", "ontem"
* → emit('goto-date', date) — pai navega o FullCalendar
* - Texto livre: pesquisa server-side em patients.nome_completo + titulo
* (via searchEventosByText) com debounce de 300ms. Limite 20 resultados.
* → emit('select-evento', ev) — pai aciona gotoDate + auto-select patient
*
* Componente autocontido — owns todo o state, debounce, parsing.
* Pai expõe um botão âncora (`.ma-cal__search-btn`) e chama
* `popoverRef.value.toggle($event)` no click. Hotkey Cmd+K também
* vive no pai (acha o botão via querySelector e chama toggle).
*
* Emit:
* - goto-date(date: Date) — escolheu uma data do parser
* - select-evento(ev) — escolheu um evento da lista de busca
*
* Exposto via defineExpose:
* - toggle(event) — abre/fecha o popover, foca input quando abre
*/
import { ref, onBeforeUnmount } from 'vue';
import Popover from 'primevue/popover';
import { searchEventosByText } from './composables/useMelissaEventos';
const emit = defineEmits(['goto-date', 'select-evento']);
const popRef = ref(null);
const inputRef = ref(null);
const searchQuery = ref('');
const searchResults = ref([]);
const searchLoading = ref(false);
const searchDateMatch = ref(null); // Date | null — preenchido se query parsear como data
let _debounceTimer = null;
// Token monotonico — protege contra race condition: se o user digita "ab"
// e depois "abc", o request "ab" pode resolver DEPOIS do "abc" em conexao
// lenta. Cada request guarda seu token; ao voltar, so aplica se ainda eh
// o mais recente. Cancelamento via AbortController seria ideal mas exigiria
// searchEventosByText aceitar signal — por ora token resolve sem mexer no API.
let _searchToken = 0;
function parseSearchAsDate(str) {
const t = String(str || '').trim().toLowerCase();
if (!t) return null;
if (t === 'hoje') { const d = new Date(); d.setHours(0,0,0,0); return d; }
if (t === 'amanha' || t === 'amanhã') {
const d = new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate() + 1); return d;
}
if (t === 'ontem') {
const d = new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate() - 1); return d;
}
// DD/MM ou DD/MM/YYYY (também aceita - e .)
const m = t.match(/^(\d{1,2})[/\-.](\d{1,2})(?:[/\-.](\d{2,4}))?$/);
if (m) {
const day = parseInt(m[1], 10);
const month = parseInt(m[2], 10);
let year = parseInt(m[3] || '', 10);
if (!year || Number.isNaN(year)) year = new Date().getFullYear();
if (year < 100) year += 2000;
if (day < 1 || day > 31 || month < 1 || month > 12) return null;
const d = new Date(year, month - 1, day);
if (Number.isNaN(d.getTime())) return null;
return d;
}
return null;
}
function toggle(event) {
popRef.value?.toggle(event);
// Foco no input quando abrir (Popover anima ~150ms). PrimeVue InputText
// renderiza `<input>` direto, então `$el` já é o elemento focável.
// Tentamos algumas vezes pra cobrir mount async + transição do Popover.
let tries = 0;
const tick = () => {
const el = inputRef.value?.$el;
if (el && typeof el.focus === 'function') { el.focus(); el.select?.(); return; }
if (tries++ < 8) setTimeout(tick, 30);
};
setTimeout(tick, 80);
}
function fechar() {
try { popRef.value?.hide(); } catch {}
}
// Toda vez que o popover fecha (via ESC, click-fora, ou submit/select),
// reseta o state pra proxima abertura comecar limpa. Antes, ESC mantinha
// query+resultados "fantasmas" — UX confusa: reabrir mostrava busca
// antiga que talvez nao fizesse mais sentido.
function onPopoverHide() {
searchQuery.value = '';
searchResults.value = [];
searchDateMatch.value = null;
searchLoading.value = false;
++_searchToken; // invalida requests em flight
}
function onSearchInput() {
if (_debounceTimer) clearTimeout(_debounceTimer);
const q = searchQuery.value;
searchDateMatch.value = parseSearchAsDate(q);
// Se digitou data, mostra resultado imediato (sem hit no DB)
if (searchDateMatch.value) {
// Invalida tokens em flight pra eles nao voltarem e sobrescreverem [].
++_searchToken;
searchResults.value = [];
searchLoading.value = false;
return;
}
if (String(q || '').trim().length < 2) {
++_searchToken;
searchResults.value = [];
searchLoading.value = false;
return;
}
searchLoading.value = true;
_debounceTimer = setTimeout(async () => {
const myToken = ++_searchToken;
try {
const results = await searchEventosByText(q);
// Race guard: se outro request foi disparado depois deste,
// descarta este (out-of-order resolution).
if (myToken !== _searchToken) return;
searchResults.value = results;
} finally {
// So zera loading se este eh o ultimo request — senao deixa o
// proximo controlar o estado (evita flicker de loading=false
// entre requests sequenciais rapidos).
if (myToken === _searchToken) searchLoading.value = false;
}
}, 300);
}
function onSearchSubmit() {
// Enter: prioriza data, senão pega o primeiro resultado
if (searchDateMatch.value) {
irParaData(searchDateMatch.value);
return;
}
if (searchResults.value.length > 0) {
selecionarResultado(searchResults.value[0]);
}
}
function irParaData(date) {
emit('goto-date', date);
fechar(); // @hide handler limpa state
}
function selecionarResultado(ev) {
if (!ev?.inicio_em) return;
emit('select-evento', ev);
fechar(); // @hide handler limpa state
}
function fmtDataResultado(iso) {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', weekday: 'short' });
}
function fmtHoraResultado(iso) {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
return d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
}
onBeforeUnmount(() => {
if (_debounceTimer) clearTimeout(_debounceTimer);
});
defineExpose({ toggle, close: fechar });
</script>
<template>
<Popover ref="popRef" class="ma-tsearch-pop" @hide="onPopoverHide">
<div class="ma-tsearch flex flex-col w-[min(440px,calc(100vw-32px))] max-h-[500px] overflow-hidden">
<div class="ma-tsearch__field relative flex items-center gap-2 px-2.5 py-1.5 mx-2 mt-2 mb-1.5 bg-[var(--m-bg-soft)] border border-[var(--m-border)] rounded-[10px] flex-shrink-0 transition-[border-color,background-color] duration-[140ms] focus-within:border-[var(--p-primary-color)] focus-within:bg-[var(--m-bg-soft-hover)] focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--p-primary-color)_12%,transparent)]">
<i class="pi pi-search ma-tsearch__field-icon text-[var(--m-text-muted)] text-[0.85rem] flex-shrink-0" />
<InputText
ref="inputRef"
v-model="searchQuery"
placeholder="Data (20/04) ou nome do paciente…"
class="ma-tsearch__input"
@input="onSearchInput"
@keydown.enter="onSearchSubmit"
@keydown.esc="fechar"
/>
<button
v-if="searchQuery"
class="ma-tsearch__clear w-[22px] h-[22px] grid place-items-center border-0 bg-[var(--m-bg-medium)] text-[var(--m-text-muted)] rounded-full cursor-pointer flex-shrink-0 transition-colors duration-[140ms] hover:bg-[var(--m-border-strong)] hover:text-[var(--m-text)]"
v-tooltip.top="'Limpar'"
@click="searchQuery = ''; onSearchInput()"
>
<i class="pi pi-times text-xs" />
</button>
</div>
<!-- Resultado data (sem hit no DB) -->
<button
v-if="searchDateMatch"
class="ma-tsearch__result ma-tsearch__result--date w-auto self-stretch flex items-center gap-2.5 px-2.5 py-2 mx-2 mb-1.5 rounded-lg cursor-pointer text-left [font-family:inherit] transition-colors duration-[140ms]"
@click="irParaData(searchDateMatch)"
>
<span class="ma-tsearch__result-icon w-8 h-8 grid place-items-center rounded-lg flex-shrink-0 text-[0.78rem]"><i class="pi pi-calendar" /></span>
<span class="ma-tsearch__result-main flex-1 min-w-0 flex flex-col gap-0.5">
<span class="ma-tsearch__result-title text-[0.85rem] font-medium whitespace-nowrap overflow-hidden text-ellipsis capitalize">Ir para {{ searchDateMatch.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', weekday: 'long' }) }}</span>
<span class="ma-tsearch__result-sub text-[0.72rem] text-[var(--m-text-muted)] whitespace-nowrap overflow-hidden text-ellipsis">Pular para essa data no calendário</span>
</span>
<i class="pi pi-arrow-right text-xs opacity-50" />
</button>
<!-- Loading -->
<div v-else-if="searchLoading" class="ma-tsearch__loading flex items-center gap-2 px-3.5 py-[18px] text-[var(--m-text-muted)] text-[0.85rem]">
<i class="pi pi-spin pi-spinner" /> <span>Buscando</span>
</div>
<!-- Resultados (eventos) -->
<div v-else-if="searchResults.length > 0" class="ma-tsearch__results flex-1 min-h-0 overflow-y-auto p-1">
<button
v-for="ev in searchResults"
:key="ev.id"
class="ma-tsearch__result w-full flex items-center gap-2.5 px-2.5 py-2 border-0 bg-transparent text-[var(--m-text)] rounded-lg cursor-pointer text-left [font-family:inherit] transition-colors duration-[140ms] hover:bg-[var(--m-bg-soft-hover)] focus-visible:bg-[var(--m-bg-soft-hover)] focus-visible:outline-none"
@click="selecionarResultado(ev)"
>
<span class="ma-tsearch__result-icon w-8 h-8 grid place-items-center rounded-lg text-white flex-shrink-0 text-[0.78rem]" :style="{ background: ev.color }">
<i :class="ev.tipo === 'sessao' ? 'pi pi-user' : 'pi pi-calendar'" />
</span>
<span class="ma-tsearch__result-main flex-1 min-w-0 flex flex-col gap-0.5">
<span class="ma-tsearch__result-title text-[0.85rem] font-medium text-[var(--m-text)] whitespace-nowrap overflow-hidden text-ellipsis">{{ ev.label }}</span>
<span class="ma-tsearch__result-sub text-[0.72rem] text-[var(--m-text-muted)] whitespace-nowrap overflow-hidden text-ellipsis">
{{ fmtDataResultado(ev.inicio_em) }} · {{ fmtHoraResultado(ev.inicio_em) }}
<span v-if="ev.modalidade"> · {{ ev.modalidade }}</span>
</span>
</span>
</button>
</div>
<!-- Vazio ( buscou mas nada encontrou) -->
<div v-else-if="String(searchQuery || '').trim().length >= 2" class="ma-tsearch__empty flex flex-col items-center gap-2 px-4 py-[26px] mx-2 mb-2.5 mt-1 text-[var(--m-text-muted)] text-[0.82rem] border-[1.5px] border-dashed border-[color-mix(in_srgb,var(--p-primary-color)_22%,var(--m-border))] rounded-[14px] bg-[color-mix(in_srgb,var(--m-bg-soft)_50%,transparent)] text-center [&>i]:text-[2.2rem] [&>i]:opacity-70 [&>i]:text-[var(--p-primary-color)] [&>i]:mb-0.5">
<i class="pi pi-search-minus" />
<span class="ma-tsearch__empty-title text-[0.88rem] font-semibold text-[var(--m-text)]">Busca não encontrada</span>
<span class="ma-tsearch__empty-sub text-[0.78rem] text-[var(--m-text-muted)] leading-[1.35] [&_strong]:text-[var(--m-text)] [&_strong]:font-semibold">
Nada para "<strong>{{ searchQuery }}</strong>"
</span>
</div>
<!-- Hint inicial Message PrimeVue (auto-resolve via PrimeVueResolver) -->
<Message
v-else
severity="info"
:closable="false"
icon="pi pi-info-circle"
class="ma-tsearch__hint mx-2 mb-2.5"
>
Digite uma data (<strong>20/04</strong>, <strong>hoje</strong>, <strong>amanhã</strong>) ou
o nome do paciente (<strong>André</strong>).
</Message>
</div>
</Popover>
</template>
<style scoped>
/* Estilos que NAO migram pra utilities Tailwind:
- .ma-tsearch__input.p-inputtext: override do PrimeVue InputText pra ele
viver dentro do "field box" (zera bordas, bg, shadow padrao). Selector
composto com classe externa do PrimeVue.
- .ma-tsearch__hint :deep(.p-message-text): override de typography dentro
do componente filho Message (isolado por scope).
- .ma-tsearch__result--date: a versao "Ir para data" tem cores azul fixas
(rgb diretas, sem var()) com !important pra blindar contra hover do
base .ma-tsearch__result. State modifier mais limpo em CSS. */
.ma-tsearch__input.p-inputtext {
flex: 1;
border: none;
background: transparent;
outline: none;
box-shadow: none;
padding: 8px 0;
font-size: 0.9rem;
color: var(--m-text);
min-width: 0;
}
.ma-tsearch__input.p-inputtext:enabled:focus,
.ma-tsearch__input.p-inputtext:enabled:hover {
box-shadow: none;
border-color: transparent;
background: transparent;
}
.ma-tsearch__input.p-inputtext::placeholder { color: var(--m-text-muted); }
.ma-tsearch__hint :deep(.p-message-text) {
font-size: 0.82rem;
line-height: 1.4;
}
.ma-tsearch__hint strong { font-weight: 600; }
/* "Ir para data" — destaque azul claro independente da primary do tenant.
Cores diretas (sem color-mix com bg-soft) pra garantir contraste em
dark mode onde --m-bg-soft tem alpha 50% e diluiria o tint demais.
!important pra blindar contra hover do .ma-tsearch__result base. */
.ma-tsearch__result--date {
background: rgba(59, 130, 246, 0.16) !important;
border: 1.5px solid rgba(59, 130, 246, 0.55) !important;
}
.ma-tsearch__result--date:hover,
.ma-tsearch__result--date:focus-visible {
background: rgba(59, 130, 246, 0.26) !important;
border-color: rgba(59, 130, 246, 0.75) !important;
}
.ma-tsearch__result--date .ma-tsearch__result-icon {
background: #3b82f6 !important;
color: white !important;
}
.ma-tsearch__result--date .ma-tsearch__result-title {
color: #2563eb !important; /* blue-600 — boa leitura em ambos os modos */
}
/* Dark mode — clareia o título pra contrastar com o fundo escuro */
html.app-dark .ma-tsearch__result--date .ma-tsearch__result-title {
color: #93c5fd !important; /* blue-300 */
}
html.app-dark .ma-tsearch__result--date .ma-tsearch__result-sub {
color: rgba(147, 197, 253, 0.7) !important;
}
</style>