OYNA

Command ​

A list of everything the app can do, opened with one key and searched by typing: the fastest way around an interface for someone who keeps their hands on the keyboard.

vue
<script setup lang="ts">
import { Palette, Rocket, RotateCcw, Settings, Terminal } from '@lucide/vue'
import type { CommandItem } from 'oyna-ui'
import { ref } from 'vue'

const open = ref(false)
const commands: CommandItem[] = [
  { value: 'deploy', label: 'Deploy to production', group: 'Deploys', icon: Rocket, hint: 'v1.4.1' },
  { value: 'rollback', label: 'Roll back', group: 'Deploys', icon: RotateCcw, keywords: 'revert undo' },
  { value: 'logs', label: 'Open the logs', group: 'Deploys', icon: Terminal },
  { value: 'theme', label: 'Theme editor', group: 'Go to', icon: Palette },
  { value: 'settings', label: 'Settings', group: 'Go to', icon: Settings, keywords: 'preferences account' },
  { value: 'billing', label: 'Billing', group: 'Go to', disabled: true, hint: 'owners only' },
]

function run(item: CommandItem) {
  // item.value tells which one
}
</script>

<template>
  <OButton hotkey="KeyK" @click="open = true">Commands</OButton>
  <OCommand v-model:open="open" :items="commands" @select="run" />
</template>

The field has the focus as soon as the palette opens. The arrow keys move through the list, Enter runs the lit item, Esc closes. Choosing an item closes the palette and emits select with that item; what it does is up to you.

How it searches ​

Every typed word must occur in the item's label, group or keywords, in any order: dep prod finds "Deploy to production", and undo finds "Roll back" through its keywords. Put the words people might try, but which are not on the label, into keywords.

Opening it ​

The palette has no key of its own. Open it from a button that owns a hotkey, as above, or with useHotkey. Hotkeys are single physical keys, so the palette here opens on K; a combination such as Ctrl+K is yours to listen for.

By default the palette filters items itself. With :filter="false" it shows them as given, and tells you what is typed through v-model:query: the search is then yours — a request to a server, or one with its own idea of what comes first. An item's description is a second, dimmer line under its label, for the words around a match.

vue
<script setup lang="ts">
import type { CommandItem } from 'oyna-ui'
import { ref, watch } from 'vue'

const open = ref(false)
const query = ref('')
const results = ref<CommandItem[]>([])

watch(query, async (text) => {
  const found = await search(text)
  results.value = found.map((page) => ({
    value: page.url,
    label: page.title,
    hint: page.section,
    description: page.excerpt,
  }))
})
</script>

<template>
  <OCommand v-model:open="open" v-model:query="query" :items="results" :filter="false" title="Search" @select="go">
    <template #footer>
      <span><OKbd>↑</OKbd> <OKbd>↓</OKbd> to choose · <OKbd>Enter</OKbd> to open</span>
    </template>
  </OCommand>
</template>

The footer slot stands under the list. The search of this site (press /) is built exactly this way.

Without groups ​

Items without a group form one plain list. title, placeholder and empty-text set the words.

vue
<OCommand
  v-model:open="open"
  :items="pages"
  title="Go to"
  placeholder="A page"
  empty-text="No such page"
  @select="go"
/>

Props ​

PropTypeDefaultDescription
itemsCommandItem[]required
v-model:openbooleanfalse
titlestring'Commands'The dialog's title
placeholderstring'Type a command'
emptyTextstring'Nothing found'Shown when nothing matches the text
filterbooleantruefalse shows the items as given: you narrow them yourself
v-model:querystring''What is typed in the field

CommandItem ​

FieldTypeDescription
valuestringWhat tells the items apart
labelstringThe text of the row
groupstringItems with the same group stand together under its name
hintstringA dim note at the end of the row
descriptionstringA second, dimmer line under the label
keywordsstringExtra words the item is found by
iconComponentAn icon component, e.g. from Lucide
disabledboolean

Slots ​

SlotDescription
footerUnder the list: a hint about the keys, a link

Events ​

EventPayloadDescription
selectCommandItemAn item was chosen

Built on Reka UI Listbox, inside a dialog.