The command palette (Spotlight / Ctrl+K style) ships as a stateful
LiveComponent plus a behaviour you implement. The reusable
interaction — modes, the multi-step wizard, pagination, keyboard navigation and the
global Ctrl+K / ⌘K shortcut — lives in the component; everything
that differs per project — your commands, contexts, search and actions — is injected
through a PureAdmin.CommandPalette.Source module.
1 · Móntalo en tu layout
One instance, referenced by id. The source is a module
(below) that supplies your app's data and actions.
# In your app layout (e.g. lib/my_app_web/components/layouts/app.html.heex),
# mount the palette ONCE so Ctrl+K and every search trigger work app-wide:
<.live_component
module={PureAdmin.CommandPalette}
id="command-palette"
source={MyApp.Palette}
/>
2 · Conecta los disparadores
show_command_palette/0 is a JS command — attach it to
anything. It dispatches to the palette by id, so no LiveView event is needed.
# Any trigger opens it — no per-trigger LiveView wiring. `show_command_palette/0`
# dispatches `pa:command-palette:open` to the palette element by id.
<.navbar_search phx-click={show_command_palette()} />
<.sidebar_search phx-click={show_command_palette()} />
# ...or from your own button:
<.button phx-click={show_command_palette()}>Search…</.button>
Contrólalo desde un LiveView
Open it pre-filled, or change the display style, with send_update/2.
# Drive the global palette from any LiveView via send_update/2:
def handle_event("search", _params, socket) do
send_update(PureAdmin.CommandPalette, id: "command-palette", open_with: "/")
{:noreply, socket}
end
# Switch the step display style ("inline" | "tokens"):
send_update(PureAdmin.CommandPalette, id: "command-palette", display: "tokens")
3 · Implementa un Source
use PureAdmin.CommandPalette.Source gives overridable no-op defaults —
define only the callbacks you need.
defmodule MyApp.Palette do
use PureAdmin.CommandPalette.Source
# Commands appear under "/" and on the idle home screen.
def commands do
[
%{
id: "go",
shortcut: "/go",
aliases: ["/g"],
hotkey: "Alt+G",
name: "Go to Page",
description: "Jump to any page",
icon: "🧭",
steps: [
%{id: "page", prompt: " ", placeholder: "Type a page…", free_text: true}
]
}
]
end
# Contexts appear under ":" for scoped search.
def contexts do
[%{id: "users", shortcut: ":users", aliases: [":u"], name: "Users", icon: "👤"}]
end
# Options for a command step, already filtered by `query`.
def step_options("go", "page", query, _selections), do: search_pages(query)
def step_options(_command, _step, _query, _selections), do: []
# Search results for a scope, already filtered by `query`.
def search(:global, query), do: search_everything(query)
def search({:context, "users"}, query), do: search_users(query)
def search(_scope, _query), do: []
# What a selection / a finished command DOES — a declarative directive.
def on_select(item), do: {:navigate, item.href}
def on_complete("go", selections), do: {:navigate, List.last(selections).value}
def on_complete(_command, _selections), do: :close
end
Callbacks de origen
The contract in PureAdmin.CommandPalette.Source. Filtering is the source's
job — search/2 and step_options/4 receive the query and return
the already-filtered list; the component paginates and, in global search, prepends the
commands/contexts that match.
| Callback | Devoluciones | Propósito |
|---|---|---|
commands/0
|
[command]
| Commands shown under / and on the home screen |
contexts/0
|
[context]
| Search contexts shown under : |
step_options/4
|
[option]
| Options for (command_id, step_id, query, selections) |
search/2
|
[item]
| Results for a scope (:global | {:context, id}) + query |
on_select/1
|
directive
| What selecting a search result does |
on_complete/2
|
directive
| What finishing a command does (command_id, selections) |
Directivas
on_select/1 and on_complete/2 return a directive the
component executes — so the palette can live globally without any LiveView carrying
selection handlers.
| Directiva | Efecto |
|---|---|
{:navigate, path}
| push_navigate (closes the palette first) |
{:patch, path}
| push_patch |
{:toast, variant, title, msg}
| flash a toast (opts as a 5th element) |
:close
| just close the palette |
:noop / nil
| do nothing |
Formas de datos
| Tipo | Forma |
|---|---|
command
|
%{id, shortcut, aliases, hotkey, name, description, icon, steps}
|
step
|
%{id, prompt, placeholder, free_text}
|
context
|
%{id, shortcut, aliases, name, description, icon}
|
option
|
%{label, value, description, icon, code}
|
item
|
%{title, subtitle, icon, badge, …your keys}
|
free_text on a step lets the user submit typed text when nothing matches.
A search item can carry any extra keys your on_select/1
needs (e.g. :href).
Modos y teclado
Modos
| Prefijo | Modo | Descripción |
|---|---|---|
/
| Commands | Multi-step action wizards |
:
| Search | Scoped entity search |
(none)
| Global | Search everything |
Teclado
| Clave | Acción |
|---|---|
| Ctrl+K / ⌘K | Toggle palette |
| ↑ ↓ | Navigate results |
| ← → | Previous / next page (search modes) |
| Enter / Tab | Select or confirm free text |
| Backspace (at start) | Back to previous step |
| Esc | Back (in step/context) or close |