Command
Stable Stimulus:senren--command
command palettes
Usage example #
Copy this ERB into a Rails view after installing the component. The snippet below is the same code used by the live preview above.
app/views/.../command_example.html.erb
<div class="w-full max-w-md">
<%= render Senren::CommandComponent.new(
placeholder: "Search commands...",
items: [
{ label: "Open docs", description: "Jump to the introduction", href: "/docs", keywords: "documentation intro" },
{ label: "Add component", description: "Install a Senren component", href: "/docs/installation", keywords: "install generate" },
{ label: "Toggle theme", description: "Switch light and dark mode", keywords: "dark light appearance" }
]
) %>
</div>
Install this component #
Copy the official component into your app
This component requires Stimulus. Keep --client so the controller is copied with the ViewComponent.
Terminal
bin/rails senren:add command --client
Create a custom component with the same conventions
Use this when you need an app-specific component that follows Senren's ViewComponent and Stimulus structure. --client is required for this behavior.
Terminal
bin/rails generate senren:component command --client
Dependencies are resolved by senren:add:
dialog, input.
At a glance #
Category
Rich
Class name
Senren::CommandComponent
Stimulus
senren--command
Variants
default
Depends on
dialog, input
Pairs with
shortcut_key, dropdown_menu
Source #
app/components/senren/command_component.html.erb
<%= tag.div(**root_attrs("overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] shadow-sm", data: { controller: "senren--command" })) do %>
<% if content? && items.empty? %>
<%= content %>
<% else %>
<label for="<%= dom_id %>-input" class="sr-only"><%= label %></label>
<div class="border-b border-[hsl(var(--senren-border))] p-2">
<input id="<%= dom_id %>-input" type="search" autocomplete="off" placeholder="<%= placeholder %>" aria-label="<%= label %>" aria-controls="<%= dom_id %>-list" aria-activedescendant="" data-senren--command-target="input" data-action="input->senren--command#filter keydown->senren--command#onKey" class="h-10 w-full rounded-(--senren-radius) bg-[hsl(var(--senren-background))] px-3 text-sm text-[hsl(var(--senren-foreground))] outline-none placeholder:text-[hsl(var(--senren-muted-foreground))] focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
</div>
<div id="<%= dom_id %>-list" role="listbox" aria-label="<%= label %>" data-senren--command-target="list" class="max-h-72 overflow-y-auto p-1.5">
<% items.each_with_index do |item, index| %>
<% tag_name = item[:href].present? ? :a : :button %>
<%= tag.public_send(tag_name, href: item[:href], type: (tag_name == :button ? "button" : nil), id: item[:id], role: "option", data: { "senren--command-target": "option", action: "click->senren--command#choose", label: item[:keywords] }, class: "block w-full cursor-pointer rounded-(--senren-radius) px-3 py-2 text-left text-sm transition-colors hover:bg-[hsl(var(--senren-accent))] aria-selected:bg-[hsl(var(--senren-accent))] aria-selected:text-[hsl(var(--senren-accent-foreground))]", "aria-selected": (index.zero? ? "true" : "false")) do %>
<span class="block font-medium"><%= item[:label] %></span>
<% if item[:description].present? %>
<span class="mt-0.5 block text-xs text-[hsl(var(--senren-muted-foreground))]"><%= item[:description] %></span>
<% end %>
<% end %>
<% end %>
<div hidden data-senren--command-target="empty" class="px-3 py-8 text-center text-sm text-[hsl(var(--senren-muted-foreground))]"><%= empty_text %></div>
</div>
<% end %>
<% end %>
app/components/senren/command_component.rb
# frozen_string_literal: true
module Senren
class CommandComponent < BaseComponent
VARIANTS = { default: '' }.freeze
SIZES = { md: '' }.freeze
def initialize(items: [], placeholder: 'Type a command...', label: 'Command menu', empty_text: 'No results found.',
id: nil, class_name: nil, **html)
super(variant: :default, size: :md, class_name: class_name, **html)
@placeholder = placeholder
@label = label
@empty_text = empty_text
@dom_id = id || "senren-command-#{SecureRandom.hex(3)}"
@items = normalize_items(items)
end
attr_reader :items, :placeholder, :label, :empty_text, :dom_id
private
def normalize_items(items)
Array(items).map.with_index do |item, index|
data = item.is_a?(Hash) ? item : { label: item.to_s }
label = data[:label] || data['label']
description = data[:description] || data['description']
keywords = data[:keywords] || data['keywords']
{
id: data[:id] || data['id'] || "#{dom_id}-option-#{index}",
label: label,
description: description,
href: data[:href] || data['href'],
keywords: [label, description, keywords].flatten.compact.join(' ')
}
end
end
end
end
AI agent rules #
Use for
- +command palettes
- +quick search
Avoid
- -persistent navigation
Accessibility #
- aria-activedescendant for current option; type-ahead filtering.