MelissaMenu: busca no topo do mm-side (estilo rail)

Input com pi-search a esquerda + botao limpar a direita. Quando
query tem texto, substitui a lista de categorias por uma lista
flat de sub-itens que casam (com nome da categoria a direita
como breadcrumb). Click no resultado dispara clicarSubItem (mesma
logica de navegacao) e limpa o termo. Empty state pra "nenhum
resultado".

Visual segue mm-aside: bg --m-bg-soft, border --m-border, focus
border --p-primary-color. Hover dos resultados usa color-mix
primary 12% (mesmo pattern do .mm-foot-item).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-06 14:33:58 -03:00
parent 0dd070c6a5
commit 9c6d77ec56
+182 -1
View File
@@ -188,6 +188,31 @@ function categoryKeyFor(itemKey) {
const selectedKey = ref(categoryKeyFor(props.secaoAtiva) || CATEGORIAS[0].key);
const copiado = ref(false);
// Busca — quando query tem texto, substitui a lista de categorias
// por uma lista flat de sub-itens que casam com o termo.
const query = ref('');
const searchResults = computed(() => {
const q = query.value.trim().toLowerCase();
if (!q) return [];
const out = [];
for (const cat of CATEGORIAS) {
for (const group of cat.groups) {
for (const item of group.items) {
if (item.tipo === 'link-cadastro') continue;
const label = (item.label || '').toLowerCase();
if (label.includes(q)) {
out.push({ catLabel: cat.label, item });
}
}
}
}
return out;
});
function clicarResultado(item) {
clicarSubItem(item);
query.value = '';
}
// Sincroniza o destaque com a sessao ativa — se o user troca de
// "Lancamentos" pra "Agenda" sem fechar o menu, o selecionado
// acompanha a sessao corrente.
@@ -382,7 +407,48 @@ async function sair() {
</button>
</div>
<div class="mm-side__list">
<!-- Busca (estilo rail) filtra sub-itens de todas as categorias -->
<div class="mm-search">
<i class="pi pi-search mm-search__icon" />
<input
v-model="query"
type="text"
inputmode="search"
autocomplete="off"
spellcheck="false"
placeholder="Encontrar menu..."
class="mm-search__input"
/>
<button
v-if="query"
class="mm-search__clear"
type="button"
aria-label="Limpar busca"
@click="query = ''"
>
<i class="pi pi-times" />
</button>
</div>
<!-- Resultados de busca (flat, com nome da categoria) -->
<div v-if="query.trim()" class="mm-search__results">
<button
v-for="r in searchResults"
:key="`${r.catLabel}_${r.item.key}`"
class="mm-search__result"
@click="clicarResultado(r.item)"
>
<i :class="r.item.icon" class="mm-search__result-icon" />
<span class="mm-search__result-label">{{ r.item.label }}</span>
<span class="mm-search__result-cat">{{ r.catLabel }}</span>
</button>
<div v-if="!searchResults.length" class="mm-search__empty">
Nenhum resultado pra "{{ query }}"
</div>
</div>
<!-- Lista normal de categorias (so quando busca vazia) -->
<div v-else class="mm-side__list">
<button
v-for="c in CATEGORIAS"
:key="c.key"
@@ -724,6 +790,121 @@ async function sair() {
transition: background-color 140ms ease;
}
.mm-side__close:hover { background: var(--m-bg-soft-hover); }
/* Busca no topo do mm-side — espelha o pattern do AppMenu rail */
.mm-search {
position: relative;
margin: 0 8px 8px;
display: flex;
align-items: center;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-radius: 10px;
transition: border-color 140ms ease, background-color 140ms ease;
}
.mm-search:focus-within {
border-color: var(--p-primary-color);
background: var(--m-bg-medium);
}
.mm-search__icon {
position: absolute;
left: 10px;
color: var(--m-text-muted);
font-size: 0.85rem;
pointer-events: none;
}
.mm-search:focus-within .mm-search__icon { color: var(--p-primary-color); }
.mm-search__input {
width: 100%;
padding: 8px 30px 8px 32px;
background: transparent;
border: none;
color: var(--m-text);
font-family: inherit;
font-size: 0.85rem;
outline: none;
}
.mm-search__input::placeholder { color: var(--m-text-faint); }
.mm-search__clear {
position: absolute;
right: 4px;
background: transparent;
border: none;
color: var(--m-text-muted);
cursor: pointer;
padding: 4px 6px;
border-radius: 6px;
display: grid;
place-items: center;
font-size: 0.7rem;
transition: color 120ms ease, background-color 120ms ease;
}
.mm-search__clear:hover {
background: var(--m-bg-soft-hover);
color: var(--m-text);
}
.mm-search__results {
flex: 1;
overflow-y: auto;
padding: 0 8px 8px;
display: flex;
flex-direction: column;
gap: 2px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mm-search__results::-webkit-scrollbar { width: 5px; }
.mm-search__results::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mm-search__result {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
background: transparent;
border: none;
border-radius: 8px;
color: var(--m-text);
text-align: left;
cursor: pointer;
font-family: inherit;
font-size: 0.85rem;
transition: background-color 120ms ease, color 120ms ease;
}
.mm-search__result:hover {
background: color-mix(in srgb, var(--p-primary-color) 12%, transparent);
color: var(--p-primary-color);
}
.mm-search__result-icon {
width: 16px;
text-align: center;
color: var(--p-primary-color);
font-size: 0.85rem;
flex-shrink: 0;
}
.mm-search__result-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mm-search__result-cat {
font-size: 0.68rem;
color: var(--m-text-muted);
flex-shrink: 0;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.mm-search__empty {
padding: 16px 12px;
color: var(--m-text-muted);
text-align: center;
font-size: 0.8rem;
}
.mm-side__list {
flex: 1;
overflow-y: auto;