Views
Push lists, grids, Markdown documents, forms and AI chats onto the navigation stack.
Commands can open views. Views stack like pages: Esc or ⌫ on an empty input goes back, and the header shows a breadcrumb. There are five types:
| Helper | Shows |
|---|---|
list() |
A searchable list, optionally with a Markdown side panel |
grid() |
A grid of images or icons |
detail() |
A Markdown document with optional metadata. It can stream. |
form() |
Fields with validation |
chat() |
An AI conversation |
The helpers only add a type field. list({...}) is the same as { type: 'list', ... }.
import { list, grid, detail, form, chat } from '@qikbar/qikbar'
Opening a view
// As a command's primary action
{ title: 'Projects', view: list({ items: projects }) }
// Lazily: build the view when it opens
{ title: 'Projects', view: async (ctx) => list({ items: await api.projects() }) }
// Return it from run or an action
{ title: 'Projects', run: () => list({ items: projects }) }
// Imperatively
ctx.push(view)
bar.push(view)
bar.open({ view })
List
list({
title: 'Issues', // breadcrumb
placeholder: 'Search issues…',
items: [ { title: 'Fix login', subtitle: '#142', accessories: [{ tag: 'bug' }] } ],
detail: true, // show item.detail Markdown in a side panel
emptyView: { title: 'No issues', description: 'You are all caught up', icon: 'check-circle' },
actions: [ /* actions available on every item */ ],
})
Async items
Pass a function to load items. It receives the current query and an AbortSignal, and is debounced (debounce, 120 ms by default). With a loader, local filtering is off by default because your loader handles the query. Set filter: true to rank results locally.
list({
items: async ({ query, signal }) => {
const res = await fetch(`/api/issues?q=${query}`, { signal })
return (await res.json()).map(toItem)
},
})
Streaming items
Return an async iterable to show results progressively, for example from pagination or a slow backend:
list({
async *items({ query }) {
let page = 1
let all = []
while (page <= 3) {
all = [...all, ...(await api.search(query, page++))]
yield all
}
},
})
Sections
Items with a section field are grouped under headings, in the order the sections first appear.
Detail panel
With detail: true, the selected item’s detail field is rendered as Markdown next to the list. detail can be a string or an async function, and results are cached per item.
{ title: 'README.md', detail: async () => await fetchReadme() }
Grid
grid({
title: 'Team',
columns: 5, // default 5
aspectRatio: '1', // or '16/9'
fit: 'cover', // or 'contain'
items: people.map((p) => ({ title: p.name, subtitle: p.role, image: p.avatar })),
})
Arrow keys move in two dimensions. items can be a loader, like in lists.
Detail
detail({
title: 'Release notes',
markdown: '# v2.0\n\n- New things',
metadata: [
{ label: 'Version', value: '2.0.0' },
{ label: 'Status', tag: { value: 'Stable', color: 'green' } },
{ label: 'Docs', value: 'Read more', href: '/docs' },
],
actions: [action.copy(text, { title: 'Copy release notes' })],
})
markdown can be a string, an async function, or an async iterable of chunks, so you can stream AI output straight into it:
{
title: 'Summarize this page',
run: (ctx) => detail({ title: 'Summary', markdown: ctx.ai!.stream(`Summarize: ${document.body.innerText}`) }),
}
Markdown is rendered safely. HTML is escaped and javascript: links are removed. It supports headings, lists, task lists, tables, code blocks with a copy button, quotes, links and images.
Form
form({
title: 'New issue',
submitLabel: 'Create issue',
fields: [
{ name: 'title', label: 'Title', required: true, placeholder: 'Short summary' },
{ name: 'body', label: 'Description', type: 'textarea' },
{ name: 'priority', label: 'Priority', type: 'select', options: [{ title: 'High', value: 'high' }, { title: 'Low', value: 'low' }] },
{ name: 'due', label: 'Due', type: 'date' },
{ name: 'notify', label: 'Notify the team', type: 'checkbox', defaultValue: true },
{ name: 'estimate', label: 'Estimate', type: 'number', validate: (v) => (v > 40 ? 'Too big, split it up' : undefined) },
],
async onSubmit(values, ctx) {
await api.createIssue(values)
ctx.toast({ title: 'Issue created', style: 'success' })
},
})
Field types: text, textarea, password, number, email, url, checkbox, select and date.
Submit with ⌘↵, or ↵ in a single-line field. Like actions, onSubmit closes the bar when it finishes. Return false to stay, or return a view to push it.
Chat
chat({ title: 'Support', prompt: 'How do I export my data?', system: 'You are Acme support.', tools: false })
It opens an AI conversation and sends prompt immediately if you set one. Requires AI. bar.ask(prompt) is a shortcut.
Re-rendering
Call ctx.render(view) to replace the current view, for example after changing data. The search text and selection are kept:
const todos = () =>
list({
title: 'Todos',
items: store.todos.map((t) => ({
title: t.text,
icon: t.done ? 'check-circle' : 'circle',
run: (ctx) => {
store.toggle(t.id)
ctx.render(todos())
return false
},
})),
})
Lifecycle
- Every view’s
ctx.signalis aborted when the view is popped or the bar closes. Pass it tofetch. onClose()runs when the view is removed.- The navigation stack resets each time the bar opens.