first commit
This commit is contained in:
@@ -1,271 +1,483 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import AppMenuItem from './AppMenuItem.vue';
|
||||
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useLayout } from '@/layout/composables/layout'
|
||||
|
||||
const model = ref([
|
||||
{
|
||||
label: 'Home',
|
||||
items: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
icon: 'pi pi-fw pi-home',
|
||||
to: '/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'UI Components',
|
||||
path: '/uikit',
|
||||
items: [
|
||||
{
|
||||
label: 'Form Layout',
|
||||
icon: 'pi pi-fw pi-id-card',
|
||||
to: '/uikit/formlayout'
|
||||
},
|
||||
{
|
||||
label: 'Input',
|
||||
icon: 'pi pi-fw pi-check-square',
|
||||
to: '/uikit/input'
|
||||
},
|
||||
{
|
||||
label: 'Button',
|
||||
icon: 'pi pi-fw pi-mobile',
|
||||
to: '/uikit/button',
|
||||
class: 'rotated-icon'
|
||||
},
|
||||
{
|
||||
label: 'Table',
|
||||
icon: 'pi pi-fw pi-table',
|
||||
to: '/uikit/table'
|
||||
},
|
||||
{
|
||||
label: 'List',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
to: '/uikit/list'
|
||||
},
|
||||
{
|
||||
label: 'Tree',
|
||||
icon: 'pi pi-fw pi-share-alt',
|
||||
to: '/uikit/tree'
|
||||
},
|
||||
{
|
||||
label: 'Panel',
|
||||
icon: 'pi pi-fw pi-tablet',
|
||||
to: '/uikit/panel'
|
||||
},
|
||||
{
|
||||
label: 'Overlay',
|
||||
icon: 'pi pi-fw pi-clone',
|
||||
to: '/uikit/overlay'
|
||||
},
|
||||
{
|
||||
label: 'Media',
|
||||
icon: 'pi pi-fw pi-image',
|
||||
to: '/uikit/media'
|
||||
},
|
||||
{
|
||||
label: 'Menu',
|
||||
icon: 'pi pi-fw pi-bars',
|
||||
to: '/uikit/menu'
|
||||
},
|
||||
{
|
||||
label: 'Message',
|
||||
icon: 'pi pi-fw pi-comment',
|
||||
to: '/uikit/message'
|
||||
},
|
||||
{
|
||||
label: 'File',
|
||||
icon: 'pi pi-fw pi-file',
|
||||
to: '/uikit/file'
|
||||
},
|
||||
{
|
||||
label: 'Chart',
|
||||
icon: 'pi pi-fw pi-chart-bar',
|
||||
to: '/uikit/charts'
|
||||
},
|
||||
{
|
||||
label: 'Timeline',
|
||||
icon: 'pi pi-fw pi-calendar',
|
||||
to: '/uikit/timeline'
|
||||
},
|
||||
{
|
||||
label: 'Misc',
|
||||
icon: 'pi pi-fw pi-circle',
|
||||
to: '/uikit/misc'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Prime Blocks',
|
||||
icon: 'pi pi-fw pi-prime',
|
||||
path: '/blocks',
|
||||
items: [
|
||||
{
|
||||
label: 'Free Blocks',
|
||||
icon: 'pi pi-fw pi-eye',
|
||||
to: '/blocks/free'
|
||||
},
|
||||
{
|
||||
label: 'All Blocks',
|
||||
icon: 'pi pi-fw pi-globe',
|
||||
url: 'https://blocks.primevue.org/',
|
||||
target: '_blank'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Pages',
|
||||
icon: 'pi pi-fw pi-briefcase',
|
||||
path: '/pages',
|
||||
items: [
|
||||
{
|
||||
label: 'Landing',
|
||||
icon: 'pi pi-fw pi-globe',
|
||||
to: '/landing'
|
||||
},
|
||||
{
|
||||
label: 'Auth',
|
||||
icon: 'pi pi-fw pi-user',
|
||||
path: '/auth',
|
||||
items: [
|
||||
{
|
||||
label: 'Login',
|
||||
icon: 'pi pi-fw pi-sign-in',
|
||||
to: '/auth/login'
|
||||
},
|
||||
{
|
||||
label: 'Error',
|
||||
icon: 'pi pi-fw pi-times-circle',
|
||||
to: '/auth/error'
|
||||
},
|
||||
{
|
||||
label: 'Access Denied',
|
||||
icon: 'pi pi-fw pi-lock',
|
||||
to: '/auth/access'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Crud',
|
||||
icon: 'pi pi-fw pi-pencil',
|
||||
to: '/pages/crud'
|
||||
},
|
||||
{
|
||||
label: 'Not Found',
|
||||
icon: 'pi pi-fw pi-exclamation-circle',
|
||||
to: '/pages/notfound'
|
||||
},
|
||||
{
|
||||
label: 'Empty',
|
||||
icon: 'pi pi-fw pi-circle-off',
|
||||
to: '/pages/empty'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Hierarchy',
|
||||
icon: 'pi pi-fw pi-align-left',
|
||||
path: '/hierarchy',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 1',
|
||||
icon: 'pi pi-fw pi-align-left',
|
||||
path: '/submenu_1',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 1.1',
|
||||
icon: 'pi pi-fw pi-align-left',
|
||||
path: '/submenu_1_1',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 1.1.1',
|
||||
icon: 'pi pi-fw pi-align-left'
|
||||
},
|
||||
{
|
||||
label: 'Submenu 1.1.2',
|
||||
icon: 'pi pi-fw pi-align-left'
|
||||
},
|
||||
{
|
||||
label: 'Submenu 1.1.3',
|
||||
icon: 'pi pi-fw pi-align-left'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Submenu 1.2',
|
||||
icon: 'pi pi-fw pi-align-left',
|
||||
path: '/submenu_1_2',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 1.2.1',
|
||||
icon: 'pi pi-fw pi-align-left'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Submenu 2',
|
||||
icon: 'pi pi-fw pi-align-left',
|
||||
path: '/submenu_2',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 2.1',
|
||||
icon: 'pi pi-fw pi-align-left',
|
||||
path: '/submenu_2_1',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 2.1.1',
|
||||
icon: 'pi pi-fw pi-align-left'
|
||||
},
|
||||
{
|
||||
label: 'Submenu 2.1.2',
|
||||
icon: 'pi pi-fw pi-align-left'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Submenu 2.2',
|
||||
icon: 'pi pi-fw pi-align-left',
|
||||
path: '/submenu_2_2',
|
||||
items: [
|
||||
{
|
||||
label: 'Submenu 2.2.1',
|
||||
icon: 'pi pi-fw pi-align-left'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Get Started',
|
||||
path: '/start',
|
||||
items: [
|
||||
{
|
||||
label: 'Documentation',
|
||||
icon: 'pi pi-fw pi-book',
|
||||
to: '/start/documentation'
|
||||
},
|
||||
{
|
||||
label: 'View Source',
|
||||
icon: 'pi pi-fw pi-github',
|
||||
url: 'https://github.com/primefaces/sakai-vue',
|
||||
target: '_blank'
|
||||
}
|
||||
]
|
||||
import AppMenuItem from './AppMenuItem.vue'
|
||||
import AppMenuFooterPanel from './AppMenuFooterPanel.vue'
|
||||
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue'
|
||||
|
||||
import FloatLabel from 'primevue/floatlabel'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
import { sessionRole, sessionIsSaasAdmin } from '@/app/session'
|
||||
import { getMenuByRole } from '@/navigation'
|
||||
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { layoutState } = useLayout()
|
||||
|
||||
const tenantStore = useTenantStore()
|
||||
const entitlementsStore = useEntitlementsStore()
|
||||
|
||||
const model = computed(() => {
|
||||
const base = getMenuByRole(sessionRole.value, { isSaasAdmin: sessionIsSaasAdmin.value }) || []
|
||||
|
||||
const normalize = (s) => String(s || '').toLowerCase()
|
||||
const priorityOrder = (group) => {
|
||||
const label = normalize(group?.label)
|
||||
if (label.includes('saas')) return 0
|
||||
if (label.includes('pacientes')) return 1
|
||||
return 99
|
||||
}
|
||||
|
||||
return [...base].sort((a, b) => priorityOrder(a) - priorityOrder(b))
|
||||
})
|
||||
|
||||
const tenantId = computed(() => tenantStore.activeTenantId || null)
|
||||
|
||||
watch(
|
||||
tenantId,
|
||||
async (id) => {
|
||||
entitlementsStore.invalidate()
|
||||
if (id) await entitlementsStore.loadForTenant(id, { force: true })
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => sessionRole.value,
|
||||
async () => {
|
||||
if (!tenantId.value) return
|
||||
entitlementsStore.invalidate()
|
||||
await entitlementsStore.loadForTenant(tenantId.value, { force: true })
|
||||
}
|
||||
)
|
||||
|
||||
// ✅ rota -> activePath (NÃO fecha menu em nenhum cenário)
|
||||
watch(
|
||||
() => route.path,
|
||||
(p) => { layoutState.activePath = p },
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// ==============================
|
||||
// 🔎 Busca no menu (flatten + resultados)
|
||||
// ==============================
|
||||
const query = ref('')
|
||||
const showResults = ref(false)
|
||||
const activeIndex = ref(-1)
|
||||
|
||||
// ✅ garante Ctrl/Cmd+K mesmo sem recentes
|
||||
const forcedOpen = ref(false)
|
||||
|
||||
// ref do InputText (pra Ctrl/Cmd + K)
|
||||
const searchEl = ref(null)
|
||||
|
||||
// wrapper pra click-outside
|
||||
const searchWrapEl = ref(null)
|
||||
|
||||
// Recentes
|
||||
const RECENT_KEY = 'menu_search_recent'
|
||||
const recent = ref([])
|
||||
|
||||
function loadRecent () {
|
||||
try { recent.value = JSON.parse(localStorage.getItem(RECENT_KEY) || '[]') } catch { recent.value = [] }
|
||||
}
|
||||
function saveRecent (q) {
|
||||
const v = String(q || '').trim()
|
||||
if (!v) return
|
||||
const list = [v, ...recent.value.filter(x => x !== v)].slice(0, 8)
|
||||
recent.value = list
|
||||
localStorage.setItem(RECENT_KEY, JSON.stringify(list))
|
||||
}
|
||||
function clearRecent () {
|
||||
recent.value = []
|
||||
try { localStorage.removeItem(RECENT_KEY) } catch {}
|
||||
}
|
||||
loadRecent()
|
||||
|
||||
watch(query, (v) => {
|
||||
const hasText = !!v?.trim()
|
||||
|
||||
// digitou: abre e sai do modo "forced"
|
||||
if (hasText) {
|
||||
forcedOpen.value = false
|
||||
showResults.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// vazio: mantém aberto apenas se foi "forçado" (Ctrl/Cmd+K ou foco)
|
||||
showResults.value = forcedOpen.value
|
||||
})
|
||||
|
||||
function clearSearch () {
|
||||
query.value = ''
|
||||
activeIndex.value = -1
|
||||
showResults.value = false
|
||||
forcedOpen.value = false
|
||||
}
|
||||
|
||||
function norm (s) {
|
||||
return String(s || '')
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function flattenMenu (items, trail = []) {
|
||||
const out = []
|
||||
for (const it of (items || [])) {
|
||||
if (it?.visible === false) continue
|
||||
|
||||
const nextTrail = [...trail, it?.label].filter(Boolean)
|
||||
|
||||
if (it?.to && !it?.items?.length) {
|
||||
out.push({
|
||||
label: it.label || it.to,
|
||||
to: it.to,
|
||||
icon: it.icon,
|
||||
trail: nextTrail,
|
||||
proBadge: !!it.proBadge,
|
||||
feature: it.feature || null
|
||||
})
|
||||
}
|
||||
]);
|
||||
|
||||
if (it?.items?.length) {
|
||||
out.push(...flattenMenu(it.items, nextTrail))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const allLinks = computed(() => flattenMenu(model.value))
|
||||
|
||||
const results = computed(() => {
|
||||
const q = norm(query.value)
|
||||
if (!q) return []
|
||||
|
||||
const wantPro = q === 'pro' || q.startsWith('pro ') || q.includes(' pro')
|
||||
|
||||
return allLinks.value
|
||||
.filter(r => {
|
||||
const hay = `${norm(r.label)} ${norm(r.trail.join(' > '))} ${norm(r.to)}`
|
||||
if (hay.includes(q)) return true
|
||||
if (wantPro && (r.proBadge || r.feature)) return true
|
||||
return false
|
||||
})
|
||||
.slice(0, 12)
|
||||
})
|
||||
|
||||
watch(results, (list) => {
|
||||
activeIndex.value = list.length ? 0 : -1
|
||||
})
|
||||
|
||||
// ===== highlight =====
|
||||
function escapeHtml (s) {
|
||||
return String(s || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function highlight (text, q) {
|
||||
const queryNorm = norm(q)
|
||||
const raw = String(text || '')
|
||||
if (!queryNorm) return escapeHtml(raw)
|
||||
|
||||
const rawNorm = norm(raw)
|
||||
const idx = rawNorm.indexOf(queryNorm)
|
||||
if (idx < 0) return escapeHtml(raw)
|
||||
|
||||
const before = escapeHtml(raw.slice(0, idx))
|
||||
const mid = escapeHtml(raw.slice(idx, idx + queryNorm.length))
|
||||
const after = escapeHtml(raw.slice(idx + queryNorm.length))
|
||||
return `${before}<mark class="px-1 rounded bg-yellow-200/40">${mid}</mark>${after}`
|
||||
}
|
||||
|
||||
// ===== teclado =====
|
||||
function onSearchKeydown (e) {
|
||||
if (e.key === 'Escape') {
|
||||
showResults.value = false
|
||||
forcedOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
if (!results.value.length) return
|
||||
showResults.value = true
|
||||
activeIndex.value = (activeIndex.value + 1) % results.value.length
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
if (!results.value.length) return
|
||||
showResults.value = true
|
||||
activeIndex.value = (activeIndex.value - 1 + results.value.length) % results.value.length
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
if (showResults.value && results.value.length && activeIndex.value >= 0) {
|
||||
e.preventDefault()
|
||||
goTo(results.value[activeIndex.value])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isTypingTarget (el) {
|
||||
if (!el) return false
|
||||
const tag = (el.tagName || '').toLowerCase()
|
||||
return tag === 'input' || tag === 'textarea' || el.isContentEditable
|
||||
}
|
||||
|
||||
// ===== Ctrl/Cmd + K =====
|
||||
function focusSearch () {
|
||||
forcedOpen.value = true
|
||||
showResults.value = true
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const inst = searchEl.value
|
||||
const input =
|
||||
inst?.$el?.tagName === 'INPUT'
|
||||
? inst.$el
|
||||
: inst?.$el?.querySelector?.('input')
|
||||
|
||||
input?.focus?.()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function onGlobalKeydown (e) {
|
||||
if (isTypingTarget(document.activeElement)) return
|
||||
|
||||
const isK = e.key?.toLowerCase() === 'k'
|
||||
const withCmdOrCtrl = e.ctrlKey || e.metaKey
|
||||
|
||||
if (withCmdOrCtrl && isK) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
focusSearch()
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Recentes: aplicar query + abrir + focar (sem depender de watch timing)
|
||||
function applyRecent (q) {
|
||||
query.value = q
|
||||
forcedOpen.value = true
|
||||
showResults.value = true
|
||||
activeIndex.value = 0
|
||||
|
||||
nextTick(() => {
|
||||
// garante foco e teclado funcionando
|
||||
focusSearch()
|
||||
})
|
||||
}
|
||||
|
||||
// click outside para fechar painel
|
||||
function onDocMouseDown (e) {
|
||||
if (!showResults.value) return
|
||||
const root = searchWrapEl.value
|
||||
if (!root) return
|
||||
|
||||
if (!root.contains(e.target)) {
|
||||
showResults.value = false
|
||||
forcedOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', onGlobalKeydown, true)
|
||||
document.addEventListener('mousedown', onDocMouseDown)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onGlobalKeydown, true)
|
||||
document.removeEventListener('mousedown', onDocMouseDown)
|
||||
})
|
||||
|
||||
async function goTo (r) {
|
||||
saveRecent(query.value)
|
||||
|
||||
query.value = ''
|
||||
showResults.value = false
|
||||
activeIndex.value = -1
|
||||
forcedOpen.value = false
|
||||
|
||||
await router.push(r.to)
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// Quick create
|
||||
// ==============================
|
||||
const quickDialog = ref(false)
|
||||
function onQuickCreate () { quickDialog.value = true }
|
||||
function onQuickCreated () { quickDialog.value = false }
|
||||
|
||||
// controle de “recentes”: mostrar ao focar (mesmo sem recentes, para exibir dicas)
|
||||
function onSearchFocus () {
|
||||
if (!query.value?.trim()) {
|
||||
forcedOpen.value = true
|
||||
showResults.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="layout-menu">
|
||||
<template v-for="(item, i) in model" :key="item">
|
||||
<app-menu-item v-if="!item.separator" :item="item" :index="i"></app-menu-item>
|
||||
<li v-if="item.separator" class="menu-separator"></li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- 🔎 TOPO FIXO -->
|
||||
<div ref="searchWrapEl" class="pb-2 border-b border-[var(--surface-border)] bg-[var(--surface-card)]">
|
||||
<div class="relative">
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText
|
||||
ref="searchEl"
|
||||
id="menu_search"
|
||||
v-model="query"
|
||||
class="w-full pr-10"
|
||||
variant="filled"
|
||||
@focus="onSearchFocus"
|
||||
@keydown="onSearchKeydown"
|
||||
/>
|
||||
</IconField>
|
||||
<label for="menu_search">Buscar no menu</label>
|
||||
</FloatLabel>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<!-- ✅ botão limpar busca -->
|
||||
<button
|
||||
v-if="query.trim()"
|
||||
type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 opacity-70 hover:opacity-100"
|
||||
@mousedown.prevent="clearSearch"
|
||||
aria-label="Limpar busca"
|
||||
>
|
||||
<i class="pi pi-times" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Recentes (quando query vazio) -->
|
||||
<div
|
||||
v-if="showResults && !query.trim() && recent.length"
|
||||
class="mt-2 border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] shadow-sm overflow-hidden"
|
||||
>
|
||||
<div class="px-3 py-2 text-xs opacity-70 flex items-center justify-content-between">
|
||||
<span>Recentes</span>
|
||||
<button
|
||||
type="button"
|
||||
class="opacity-70 hover:opacity-100"
|
||||
@mousedown.prevent="clearRecent"
|
||||
aria-label="Limpar recentes"
|
||||
>
|
||||
<i class="pi pi-trash" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-for="q in recent"
|
||||
:key="q"
|
||||
class="w-full text-left px-3 py-2 hover:bg-[var(--surface-hover)] flex items-center gap-2"
|
||||
type="button"
|
||||
@click.stop.prevent="applyRecent(q)"
|
||||
>
|
||||
<i class="pi pi-history opacity-70" />
|
||||
<div class="flex-1">{{ q }}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Resultados -->
|
||||
<div
|
||||
v-else-if="showResults && results.length"
|
||||
class="mt-2 border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] shadow-sm overflow-hidden"
|
||||
>
|
||||
<button
|
||||
v-for="(r, i) in results"
|
||||
:key="r.to"
|
||||
type="button"
|
||||
@mousedown.prevent="goTo(r)"
|
||||
:class="[
|
||||
'w-full text-left px-3 py-2 flex items-center gap-2',
|
||||
i === activeIndex ? 'bg-[var(--surface-hover)]' : 'hover:bg-[var(--surface-hover)]'
|
||||
]"
|
||||
>
|
||||
<i v-if="r.icon" :class="r.icon" class="opacity-80" />
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="font-medium leading-tight" v-html="highlight(r.label, query)" />
|
||||
<small class="opacity-70">{{ r.trail.join(' > ') }}</small>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="r.proBadge || r.feature"
|
||||
class="text-xs px-2 py-0.5 rounded border border-[var(--surface-border)] opacity-80"
|
||||
>
|
||||
PRO
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="showResults && query && !results.length"
|
||||
class="mt-2 px-3 py-2 text-sm opacity-70"
|
||||
>
|
||||
Nenhum item encontrado.
|
||||
</div>
|
||||
|
||||
<!-- ✅ instruções embaixo quando houver recentes/resultados/uso -->
|
||||
<div
|
||||
v-if="showResults && (recent.length || results.length || query.trim())"
|
||||
class="mt-2 px-3 text-xs opacity-70 flex flex-wrap gap-x-3 gap-y-1"
|
||||
>
|
||||
<span><b>Ctrl+K</b>/<b>Cmd+K</b> focar</span>
|
||||
<span><b>↑↓</b> navegar</span>
|
||||
<span><b>Enter</b> abrir</span>
|
||||
<span><b>Esc</b> fechar</span>
|
||||
</div>
|
||||
|
||||
<!-- fallback quando não tem nada -->
|
||||
<div
|
||||
v-else-if="showResults && !query.trim() && !recent.length"
|
||||
class="mt-2 px-3 py-2 text-xs opacity-60"
|
||||
>
|
||||
Dica: pressione <b>Ctrl + K</b> (ou <b>Cmd + K</b>) para buscar.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ SOMENTE O MENU ROLA -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<ul class="layout-menu pb-20">
|
||||
<template v-for="(item, i) in model" :key="i">
|
||||
<AppMenuItem
|
||||
:item="item"
|
||||
:index="i"
|
||||
:root="true"
|
||||
@quick-create="onQuickCreate"
|
||||
/>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- rodapé fixo -->
|
||||
<AppMenuFooterPanel />
|
||||
|
||||
<ComponentCadastroRapido
|
||||
v-model="quickDialog"
|
||||
title="Cadastro Rápido"
|
||||
table-name="patients"
|
||||
name-field="nome_completo"
|
||||
email-field="email_principal"
|
||||
phone-field="telefone"
|
||||
:extra-payload="{ status: 'Ativo' }"
|
||||
@created="onQuickCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user