Components

CommandPalette

Tavolozza comandi stile Cmd+K con ricerca, fonti asincrone, navigazione da tastiera e accessibilità ARIA.

Panoramica

CommandPalette è il componente UI della command palette. Si apre e si chiude con Ctrl+K (Windows/Linux) o Cmd+K (macOS), oppure con open/close/toggle di useCommandPalette, e mostra i comandi registrati e i risultati delle fonti asincrone.

Non riceve comandi né stato come props: li legge dal singleton di useCommandPalette. Va montato una sola volta, di solito nel layout dell'applicazione. Usa <Teleport to="body">, quindi la posizione nel template è indifferente.

Anteprima live

Clicca il pulsante o premi Ctrl+K / Cmd+K. Prova «ticket» (comandi registrati), «lavaggio» (fonte asincrona con 400 ms di latenza: compare «Ricerca in corso...») e «@ro» (fonte con prefisso @, che nasconde i comandi).

Props

Tutte facoltative.

PropTipoDefaultDescrizione
labelsPartial<CommandPaletteLabels>italianoOverride parziale di testi ed etichette ARIA, per l'i18n
loadingbooleanfalseIl catalogo dei comandi è in caricamento (es. fetch del consumer): mostra «Caricamento comandi...» al posto di «nessun comando». Prevale sullo stato di caricamento delle fonti
wrapAroundbooleantrueLe frecce ripartono dall'altro capo della lista. Con false si fermano ai bordi
showFooterbooleantrueMostra il footer di aiuto alla navigazione
maxResultsnumber100Righe mostrate al massimo. Oltre compare «+N altri risultati. Affina la ricerca.»

Il tipo delle props è CommandPaletteProps.

CommandPaletteLabels

EtichettaDefault
searchPlaceholderCerca comando...
searchLabelCerca comando (aria-label dell'input)
dialogTitleTavolozza comandi (titolo del dialog per gli screen reader)
listboxLabelRisultati comandi
emptyNoCommandsNessun comando disponibile
emptyNoResults(query) => 'Nessun comando trovato per «query»'
loadingLabelCaricamento comandi... (prop loading)
sourcesLoadingRicerca in corso... (fonti asincrone in attesa)
clearSearchCancella ricerca
commandErrorEsecuzione del comando non riuscita. Riprova.
resultsAnnouncement(count, query) => string: «N comandi trovati», annunciato agli screen reader. Una stringa vuota non annuncia nulla
footerNavigate / footerSelect / footerClosenaviga / seleziona / chiudi: i verbi del footer, accanto ai tasti
footerHint / footerHintEmptyTesto intero del footer, con e senza risultati selezionabili. Se passati, sostituiscono il footer con i tasti
moreResults(hidden) => '+hidden altri risultati. Affina la ricerca.'
<CommandPalette
  :labels="{
    searchPlaceholder: 'Search command...',
    emptyNoResults: (query) => `No command for “${query}”`,
    sourcesLoading: 'Searching...',
  }"
/>

Eventi e slot

Il componente non emette eventi e non ha slot. L'esecuzione passa dall'handler del comando, gli errori da onCommandError di useCommandPalette.

Lista dei risultati

  • Senza query: tutti i comandi, raggruppati sotto un header per category (header fisso durante lo scroll)
  • Con una query: i comandi che corrispondono, in una lista piatta con la categoria a destra di ogni riga (troncata oltre la larghezza massima, testo intero nel title)
  • Risultati delle fonti: seguono i comandi, sotto l'header della loro category, man mano che arrivano
  • Ricerca in corso: finché ci sono fonti in attesa compare «Ricerca in corso...», al posto dell'empty state se non ci sono ancora risultati, altrimenti sotto la lista. «Nessun comando trovato» compare solo quando tutte le fonti hanno risposto
  • Empty state: messaggio e pulsante «Cancella ricerca»

Ogni riga mostra icon, label, description, il badge shortcut e, in ricerca, la category. Un'icona con due o più parole (pi pi-home, fa fa-user) è resa come classe di un icon font, altrimenti come testo (emoji o carattere).

Se il catalogo cambia a palette aperta, la selezione resta sullo stesso comando.

Interazioni da tastiera e mouse

TastoAzione
Ctrl+K / Cmd+KApre o chiude la palette, anche con il focus in un campo di testo
ArrowDown / ArrowUpSelezione successiva o precedente (circolare con wrapAround)
EnterEsegue il comando selezionato
EscapeCon una query la svuota, senza query chiude la palette
Tab / Shift+TabRestano dentro il dialog (focus trap)
  • Il click su una riga la esegue, il click sullo sfondo chiude la palette
  • La selezione segue il mouse solo quando si muove: una riga che compare sotto il cursore fermo non ruba la selezione alla tastiera
  • Durante la composizione IME (cinese, giapponese, coreano) la ricerca attende il testo definitivo

All'apertura il focus va sull'input e la selezione sulla prima riga; alla chiusura torna all'elemento che lo aveva prima.

Esecuzione ed errori

Durante l'esecuzione di un comando il dialog ha aria-busy="true" e un secondo Enter viene ignorato. Se l'handler termina senza errori la palette si chiude; se lancia, resta aperta con il messaggio commandError (role="alert") e l'utente può riprovare o cercare altro. Il messaggio sparisce alla ricerca successiva.

Un comando malformato che fa fallire il rendering non fa cadere l'applicazione: l'errore viene intercettato e passato a onRenderError di useCommandPalette.

Accessibilità

  • Dialog: role="dialog", aria-modal="true", titolo per gli screen reader collegato con aria-labelledby
  • Sfondo: il resto della pagina diventa inert e aria-hidden mentre la palette è aperta, e lo scroll del body è bloccato
  • Input: role="combobox" con aria-controls, aria-autocomplete="list" e aria-activedescendant sulla riga selezionata
  • Risultati: role="listbox" con righe role="option" e aria-selected; gli header di categoria sono presentazionali
  • Annunci: una regione aria-live="polite" annuncia il numero di risultati; caricamento ed empty state sono role="status"

Stile

Lo stile è fatto di utility Tailwind, con dark mode tramite le varianti dark:; il pacchetto non distribuisce CSS. Nel portale le classi vengono generate dalla shell (che scansiona il bundle della libreria e le ha nella safelist): la palette è della shell, non dei microfrontend. In un'applicazione standalone il Tailwind del progetto deve scansionare node_modules/@pzeta/vue-scorciatoie/dist.

Esempio base

<script setup lang="ts">
import { CommandPalette, useCommandPalette } from '@pzeta/vue-scorciatoie'

useCommandPalette([
  {
    id: 'apri-anagrafica',
    label: 'Apri anagrafica',
    category: 'Navigazione',
    handler: () => router.push('/anagrafica'),
  },
  {
    id: 'nuova-attivita',
    label: 'Nuova attività',
    category: 'Azioni',
    keywords: ['task', 'todo'],
    handler: () => apriDialogAttivita(),
  },
])
</script>

<template>
  <CommandPalette />
  <RouterView />
</template>

Le keyword non sono visualizzate ma partecipano alla ricerca: digitando «task» si trova «Nuova attività».

Esempio con catalogo caricato dal server

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { CommandPalette, useCommandPalette } from '@pzeta/vue-scorciatoie'

const caricamento = ref(true)
const { addCommand } = useCommandPalette()

onMounted(async () => {
  try {
    const voci: { id: string; titolo: string; percorso: string }[] = await fetch('/api/menu').then((r) => r.json())
    for (const v of voci) {
      addCommand({ id: v.id, label: v.titolo, category: 'Menu', handler: () => router.push(v.percorso) })
    }
  } finally {
    caricamento.value = false
  }
})
</script>

<template>
  <CommandPalette :loading="caricamento" />
</template>

Per risultati che dipendono dalla query (articoli, clienti, documenti) si usano invece le fonti asincrone.