Navigation

NavigationMenu

Menu di navigazione strutturato per sidebar MFE. Organizza le voci per moduli e gruppi con indicatore active automatico via Vue Router, modalità collapsed (rail solo-icone), collasso a fisarmonica opt-in per moduli e categorie, sfondo customizzabile (preset glass/gradient o classi arbitrarie) con palette adattiva, e slot per item personalizzati.

Import

import { NavigationMenu } from '@pzeta/vue-components'
import type { MenuStructure, MenuGroup } from '@pzeta/vue-components'
import type { MenuItem } from '@pzeta/vue-components'

Struttura dati

Il NavigationMenu riceve una struttura gerarchica che rappresenta uno o più moduli MFE:

interface MenuStructure {
  manifest: MFEManifest   // { name, version, icon?, description?, routes? }
  menuGroups: MenuGroup[]
}

interface MenuGroup {
  category: string        // nome della categoria/gruppo
  icon?: string
  items: MenuItem[]       // voci del gruppo (usano MenuItem standard)
}

manifest.description (opzionale) viene mostrato come sottotitolo sotto il nome del modulo, solo in stato esteso (dal 1.7.7).

Esempio Base

Props

PropTipoDefaultDescrizione
menuStructureMenuStructure[]— (required)Struttura completa del menu da renderizzare
collapsedbooleanfalseStato collapsed della sidebar (rail): nasconde label e badge, mostra solo icone
activeKeystringundefinedSelezione controllata disaccoppiata dal router (v-model:active-key). Se omessa, l'active deriva dalla route
showCollapseTogglebooleanfalseMostra il toggle interno di compressione/espansione del rail in testa al menu
collapsiblebooleanfalseAbilita l'accordion su header di modulo e label di categoria (dal 1.7.6)
expandedModulesstring[]undefinedModuli espansi controllati (v-model:expanded-modules) (dal 1.7.6)
expandedCategoriesstring[]undefinedCategorie espanse controllate (v-model:expanded-categories); chiave `${modulo}::${categoria}` (dal 1.7.6)
forceExpandedbooleanfalseForza tutto espanso ignorando il collasso: da abbinare alla ricerca esterna (dal 1.7.6)
autoExpandActivebooleantrueTiene sempre aperti modulo e categoria che contengono la voce attiva (dal 1.7.7)
persistKeystringundefinedPersiste lo stato di collasso su localStorage sotto questa chiave (solo in modalità non controllata) (dal 1.7.7)
highlightQuerystringundefinedEvidenzia nel label la porzione che combacia con la query (highlight ricerca) (dal 1.7.7)
scrollActiveIntoViewbooleanfalseScorre la voce attiva nel viewport al mount e a ogni cambio di selezione/route (dal 1.7.7)
defaultExpand'all' | 'modules' | 'none''all'Stato di espansione iniziale (non controllato): tutto aperto / solo moduli aperti / tutto chiuso (dal 1.7.8)
variant'default' | 'glass' | 'gradient''default'Preset visivo dello sfondo: glass (superficie translucida con backdrop blur, segue il tema) o gradient (gradiente primary scuro). Ignorato se background è valorizzata (dal 1.7.12)
backgroundstringundefinedSfondo custom: classi Tailwind arbitrarie applicate alla <nav> (gradiente, immagine, blur). Ha priorità sul preset variant (dal 1.7.12)
appearance'auto' | 'on-dark' | 'on-light''auto'Palette delle voci rispetto allo sfondo: auto segue il tema, on-dark testi chiari e fill translucidi bianchi, on-light forza la palette chiara. Il preset gradient attiva on-dark automaticamente (dal 1.7.12)
navClassstringundefinedClasse CSS aggiuntiva per l'elemento <nav>

Emits

EventoPayloadDescrizione
navigatepath: stringEmesso al click su un elemento; passa il path della route
update:activeKeykey: stringAggiornamento della chiave attiva (v-model:active-key)
selectitem: MenuItemVoce selezionata (utile per item command-only senza route)
update:collapsedvalue: booleanAggiornamento dello stato rail dal toggle interno (v-model:collapsed)
update:expandedModulesvalue: string[]Aggiornamento dei moduli espansi (v-model:expanded-modules) (dal 1.7.6)
update:expandedCategoriesvalue: string[]Aggiornamento delle categorie espanse (v-model:expanded-categories) (dal 1.7.6)

Slot

SlotScopeDescrizione
menu-item{ item: { path, label, icon?, order?, badge? }; active: boolean }Template personalizzato per ogni voce del menu. active è calcolato automaticamente da Vue Router

Metodi esposti (ref) (dal 1.7.8)

Accessibili via template ref (NavigationMenuExpose):

MetodoDescrizione
expandAll()Espande tutti i moduli e le categorie collassabili
collapseAll()Collassa tutti i moduli e le categorie collassabili (il ramo attivo resta aperto se autoExpandActive)
<script setup lang="ts">
import { ref } from 'vue'
import { NavigationMenu } from '@pzeta/vue-components'

const nav = ref<InstanceType<typeof NavigationMenu> | null>(null)
</script>

<template>
  <button @click="nav?.expandAll()">Espandi tutto</button>
  <button @click="nav?.collapseAll()">Comprimi tutto</button>
  <NavigationMenu ref="nav" :menu-structure="menu" collapsible default-expand="modules" />
</template>

Esempi

Modalità collapsed (sidebar compressa)

Con template voce personalizzato

<script setup lang="ts">
import type { MenuStructure } from '@pzeta/vue-components'

const menuStructure: MenuStructure[] = [
  {
    manifest: { name: 'CRM', version: '1.0.0', icon: 'users' },
    menuGroups: [
      {
        category: 'Clienti',
        items: [
          { label: 'Anagrafiche', icon: 'address-book', to: '/crm/anagrafiche' },
          { label: 'Opportunità', icon: 'chart-line', to: '/crm/opportunita' },
        ],
      },
    ],
  },
  {
    manifest: { name: 'Fatturazione', version: '1.0.0', icon: 'file-invoice' },
    menuGroups: [
      {
        category: 'Documenti',
        items: [
          { label: 'Fatture', icon: 'file-invoice', to: '/fatturazione/fatture' },
          { label: 'Note credito', icon: 'file-minus', to: '/fatturazione/note-credito' },
        ],
      },
    ],
  },
]
</script>

Accordion collassabile con ricerca (dal 1.7.6)

Con collapsible, header di modulo e categoria diventano toggle a fisarmonica. La ricerca resta esterna (filtra menuStructure): durante una ricerca attiva forceExpanded riespande i rami filtrati e highlightQuery evidenzia il testo trovato.

<script setup lang="ts">
import { ref, computed } from 'vue'
import type { MenuStructure } from '@pzeta/vue-components'

const search = ref('')
const menuStructure: MenuStructure[] = [
  {
    manifest: { name: 'CRM', version: '2.0.0', icon: 'users', description: 'Gestione clienti' },
    menuGroups: [
      { category: 'Clienti', items: [
        { label: 'Lista Clienti', icon: 'list', to: '/crm/clienti', itemKey: 'crm-clients' },
        { label: 'Nuovo Cliente', icon: 'plus', to: '/crm/nuovo', itemKey: 'crm-new' },
      ] },
      { category: 'Opportunità', items: [
        { label: 'Pipeline', icon: 'filter', to: '/crm/pipeline', itemKey: 'crm-pipeline' },
      ] },
    ],
  },
]

// Filtro esterno: la libreria non conosce la ricerca, la esegue il consumer.
const filtered = computed<MenuStructure[]>(() => {
  const q = search.value.trim().toLowerCase()
  if (!q) return menuStructure
  return menuStructure
    .map((m) => ({
      ...m,
      menuGroups: m.menuGroups
        .map((g) => ({ ...g, items: g.items.filter((i) => i.label?.toLowerCase().includes(q)) }))
        .filter((g) => g.items.length > 0),
    }))
    .filter((m) => m.menuGroups.length > 0)
})
</script>

<template>
  <div class="w-64">
    <input v-model="search" placeholder="Cerca voce..." class="w-full mb-2 px-3 py-2 rounded-lg" />
    <NavigationMenu
      :menu-structure="filtered"
      collapsible
      persist-key="crm-nav"
      scroll-active-into-view
      :force-expanded="!!search.trim()"
      :highlight-query="search"
    />
  </div>
</template>
  • autoExpandActive (default true): il modulo e la categoria che contengono la voce attiva restano sempre aperti, così la pagina corrente non è mai nascosta.
  • persistKey: lo stato di collasso viene salvato in localStorage e ripristinato al reload (solo modalità non controllata).
  • Per il controllo esplicito usa v-model:expanded-modules / v-model:expanded-categories.

Sfondo customizzabile (dal 1.7.12)

Il menu è trasparente di default (eredita lo sfondo del contenitore). Con variant si applica un preset pronto, con background classi Tailwind arbitrarie; appearance commuta la palette delle voci (testi, hover, active) per restare leggibile su qualsiasi sfondo.

Immagine sotto il glass: il backdrop-filter sfoca ciò che sta dietro la nav, quindi l'immagine va sul contenitore (o sulla contentClass della Sidebar), non sulla nav stessa:

Il frost del preset glass è calibrato leggero (blur 6px, superficie al 50%) per lasciare riconoscibile l'immagine dietro. Per un frost più coprente su immagini molto contrastate usa la prop background (es. bg-white/70 backdrop-blur-lg).

Comportamento

  • L'indicatore active viene calcolato confrontando route.path con il campo to di ogni voce.
  • Una voce è attiva se route.path === path oppure route.path.startsWith(path + '/').
  • In modalità collapsed, l'intestazione del modulo, le categorie e i label delle voci vengono nascosti; rimangono solo le icone. Il title del link viene impostato al label per accessibilità.
  • I badge supportano sia valori statici (stringa) sia funzioni reattive () => string | number.
  • Con collapsible, moduli e categorie sono collassabili in modo indipendente; l'apertura è animata (grid-template-rows) e rispetta prefers-reduced-motion. In modalità collapsed (rail) l'accordion è disattivato e tutte le icone restano visibili.
  • La voce attiva mostra una barra accent verticale sul bordo sinistro oltre al fill; gli hover sono translucidi, quindi uniformi su sfondi chiari, scuri e immagini (dal 1.7.12).
  • Se background è valorizzata il preset variant viene ignorato; con appearance: 'auto' il preset gradient commuta da solo la palette a on-dark (dal 1.7.12).

Accessibilità

  • Usa <nav> come contenitore principale.
  • Le voci sono <RouterLink> con class hs-nav-menu__link--active / hs-nav-menu__link--inactive e aria-current="page" sulla voce attiva.
  • In modalità collapsed le icone hanno il title corrispondente al label.
  • Con collapsible, gli header di modulo/categoria sono <button> con aria-expanded e aria-controls; i pannelli chiusi sono resi inert (fuori da tab-order e albero accessibile) e i toggle hanno focus-visible.

Theming (CSS variables) (dal 1.7.12)

Tutti i colori del menu passano dalle CSS variables --hs-nav-*, definite su .hs-nav-menu e ridefinite dal tema scuro e dai modifier di appearance. Per personalizzazioni avanzate (oltre variant/appearance) si possono sovrascrivere via CSS dell'app:

VariableRuolo
--hs-nav-headingTesto header modulo
--hs-nav-faintSottotitolo modulo, chevron
--hs-nav-mutedLabel categoria, toggle rail
--hs-nav-linkTesto voce inattiva
--hs-nav-hover-bgSfondo hover (translucido)
--hs-nav-active-bgSfondo voce attiva
--hs-nav-active-hover-bgSfondo hover voce attiva
--hs-nav-active-textTesto voce attiva
--hs-nav-accentBarra accent verticale della voce attiva
--hs-nav-dividerDivider modulo e guida verticale dei gruppi annidati
--hs-nav-ringFocus ring di link e toggle
--hs-nav-highlight-bg / --hs-nav-highlight-textEvidenziazione highlightQuery
/* Esempio: accent verde brand su tutta l'app */
.my-app .hs-nav-menu {
  --hs-nav-accent: var(--color-success-500);
  --hs-nav-active-text: var(--color-success-700);
}

TypeScript

import type {
  NavigationMenuProps,
  NavigationMenuEmits,
  NavigationMenuSlots,
  MenuStructure,
  MenuGroup,
  MFEManifest,
} from '@pzeta/vue-components'