Navigation

Selection Actions

An action palette for the current selection: the context is pinned above the field and the destructive action is set apart by a rule and its colour.

  • command
  • actions
  • context
  • palette

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/command-009?lang=en

LayerHero background

  • ПереименоватьF2
  • Дублировать⌘D
  • Выровнять по сетке
  • Экспортировать в SVG
  • Удалить слой
Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "command-009" (Selection Actions) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/command-009.json

Registry item: https://vibeui.ru/r/command-009.json
Installs to: components/vibeui/command-009.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
An action palette for the current selection: the context is pinned above the field and the destructive action is set apart by a rule and its colour.

A contextual palette: the object on top, search and its actions below, with the destructive one set apart. Zero dependencies, one file.

## 3. How to use it
import { Command009 } from "@/components/vibeui/command-009"

<Command009 target="Hero background" targetKind="Layer" actions={[{ label: "Delete layer", tone: "danger" }]} />

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-command-009-* palette — do not swap it for your theme tokens
- the object row above the field: a plain palette answers "what to do", a contextual one "what to do with this"
- the dialog's aria-labelledby pointing at the object row — otherwise the palette announces itself without context
- setting the destructive action apart with a rule and spacing: an arrow-key slip is too expensive
- the destructive colour in the highlight too: a red row alone disappears once selected
- the role=combobox plus aria-activedescendant pairing: focus stays in the field

## 6. You may change
- the object name through the target prop
- the object type through the targetKind prop
- the actions array and its tone flags
- the accent through the accent prop

## 7. Rules
- There is no delete confirmation here: the destructive action is only marked, add the dialog outside.
- Enter on a row runs nothing: hook up your own action handler.
- A long object name truncates with an ellipsis — mirror the full value into a title attribute.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/command-009.json
https://vibeui.ru/r/command-009.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-command-009-*. Клиентский: "use client" ради поиска и клавиатуры. Строка объекта стоит над полем и не участвует в прокрутке списка, а её id отдан в aria-labelledby диалога — так палитра объявляется вместе с объектом, к которому относится. Действия с tone="danger" получают отступ, разделительную линию и собственный цвет подсветки: промах стрелкой по удалению стоит слишком дорого. Роли combobox, listbox и option объявлены явно.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

"use client"

import { useId, useState } from "react"
import type {
  ComponentPropsWithoutRef,
  CSSProperties,
  KeyboardEvent,
} from "react"

export type Command009Action = {
  label: string
  keys?: string
  tone?: "danger"
}

export type Command009Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  target?: string
  targetKind?: string
  actions?: Command009Action[]
  accent?: string
}

// Идея компонента: палитра действий над уже выбранным объектом. Обычная
// палитра отвечает на «что сделать», контекстная — на «что сделать вот с
// этим», поэтому объект закреплён строкой над полем и не уезжает при
// прокрутке списка. Опасные действия помечены цветом и отделены линией:
// «Удалить» не должно оказаться соседом «Переименовать» без границы, иначе
// промах стрелкой стоит слишком дорого.
const STYLES = `
:where([data-vibeui-block="command-009"]){
--vibeui-command-009-bg:oklch(1 0 0);
--vibeui-command-009-fg:oklch(0.23 0.014 265);
--vibeui-command-009-muted:oklch(0.57 0.014 265);
--vibeui-command-009-border:oklch(0.9 0.006 265);
--vibeui-command-009-accent:oklch(0.55 0.2 300);
--vibeui-command-009-danger:oklch(0.55 0.2 25);
--vibeui-command-009-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="command-009"]{
display:block;box-sizing:border-box;width:100%;max-width:23rem;overflow:hidden;
background:var(--vibeui-command-009-bg);color:var(--vibeui-command-009-fg);
border:1px solid var(--vibeui-command-009-border);border-radius:0.875rem;
box-shadow:0 18px 40px -28px oklch(0.2 0.03 265 / 60%);
font-family:var(--vibeui-command-009-font);
}
/* Объект закреплён над полем: контекст не должен уезжать вместе со списком. */
[data-vibeui-block="command-009"] [data-part="target"]{
display:flex;align-items:center;gap:0.5rem;
padding:0.5rem 0.75rem;
background:color-mix(in oklab,var(--vibeui-command-009-accent) 9%,transparent);
border-bottom:1px solid var(--vibeui-command-009-border);
font-size:0.75rem;
}
[data-vibeui-block="command-009"] [data-part="kind"]{
flex:none;padding:0.0625rem 0.375rem;border-radius:0.375rem;
background:var(--vibeui-command-009-accent);color:oklch(1 0 0);
font-size:0.625rem;font-weight:750;letter-spacing:0.02em;text-transform:uppercase;
}
[data-vibeui-block="command-009"] [data-part="name"]{
min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:650;
}
[data-vibeui-block="command-009"] input{
box-sizing:border-box;width:100%;height:2.75rem;padding:0 0.875rem;
appearance:none;border:0;border-bottom:1px solid var(--vibeui-command-009-border);
background:none;color:inherit;font:inherit;font-size:0.9375rem;outline:none;
}
[data-vibeui-block="command-009"] input:focus{box-shadow:inset 0 -2px 0 0 var(--vibeui-command-009-accent)}
[data-vibeui-block="command-009"] [data-part="list"]{
list-style:none;margin:0;padding:0.3125rem;max-height:14rem;overflow-y:auto;
}
[data-vibeui-block="command-009"] [data-part="row"]{
display:flex;align-items:center;gap:0.75rem;
padding:0.4375rem 0.5625rem;border-radius:0.5rem;cursor:pointer;font-size:0.875rem;
}
[data-vibeui-block="command-009"] [data-part="row"]:hover,
[data-vibeui-block="command-009"] [data-part="row"][aria-selected="true"]{
background:color-mix(in oklab,var(--vibeui-command-009-accent) 12%,transparent);
}
[data-vibeui-block="command-009"] [data-part="row"][data-tone="danger"]{
margin-top:0.25rem;border-top:1px solid var(--vibeui-command-009-border);
border-radius:0 0 0.5rem 0.5rem;padding-top:0.5625rem;
color:var(--vibeui-command-009-danger);
}
[data-vibeui-block="command-009"] [data-part="row"][data-tone="danger"]:hover,
[data-vibeui-block="command-009"] [data-part="row"][data-tone="danger"][aria-selected="true"]{
background:color-mix(in oklab,var(--vibeui-command-009-danger) 12%,transparent);
}
[data-vibeui-block="command-009"] kbd{
margin-left:auto;
border:1px solid var(--vibeui-command-009-border);border-bottom-width:2px;border-radius:0.3125rem;
padding:0 0.3125rem;font:inherit;font-size:0.6875rem;color:var(--vibeui-command-009-muted);
}
[data-vibeui-block="command-009"] [data-part="empty"]{
margin:0;padding:1.125rem 0.875rem;font-size:0.875rem;color:var(--vibeui-command-009-muted);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="command-009"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_ACTIONS: Command009Action[] = [
  { label: "Переименовать", keys: "F2" },
  { label: "Дублировать", keys: "⌘D" },
  { label: "Выровнять по сетке" },
  { label: "Экспортировать в SVG" },
  { label: "Удалить слой", keys: "⌫", tone: "danger" },
]

/**
 * Палитра действий над выделенным объектом: контекст закреплён над полем,
 * опасное действие отделено. Один файл, ноль зависимостей.
 */
export function Command009({
  target = "Hero background",
  targetKind = "Слой",
  actions = DEFAULT_ACTIONS,
  accent,
  className,
  style,
  ...props
}: Command009Props) {
  const [query, setQuery] = useState("")
  const [active, setActive] = useState(0)
  const listId = useId()
  const rowId = useId()
  const titleId = useId()

  const needle = query.trim().toLowerCase()
  const rows = actions.filter((action) =>
    action.label.toLowerCase().includes(needle),
  )
  const current = rows[Math.min(active, rows.length - 1)]

  const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === "ArrowDown" || event.key === "ArrowUp") {
      event.preventDefault()
      if (rows.length === 0) return
      const step = event.key === "ArrowDown" ? 1 : -1
      setActive((index) => (index + step + rows.length) % rows.length)
      return
    }

    if (event.key === "Escape") {
      event.preventDefault()
      setQuery("")
      setActive(0)
    }
  }

  const paletteStyle = {
    ...(accent ? { "--vibeui-command-009-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-command-009" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="command-009"
        className={className}
        style={paletteStyle}
        role="dialog"
        aria-labelledby={titleId}
      >
        <p id={titleId} data-part="target">
          <span data-part="kind">{targetKind}</span>
          <span data-part="name">{target}</span>
        </p>
        <input
          type="text"
          role="combobox"
          aria-expanded={rows.length > 0}
          aria-controls={listId}
          aria-autocomplete="list"
          aria-activedescendant={
            current ? `${rowId}-${rows.indexOf(current)}` : undefined
          }
          aria-label={`Действие над «${target}»`}
          placeholder="Действие…"
          value={query}
          onChange={(event) => {
            setQuery(event.target.value)
            setActive(0)
          }}
          onKeyDown={onKeyDown}
        />
        {rows.length === 0 ? (
          <p data-part="empty">Для этого объекта такого действия нет.</p>
        ) : (
          <ul id={listId} data-part="list" role="listbox" aria-label="Действия">
            {rows.map((action, index) => (
              <li
                key={action.label}
                id={`${rowId}-${index}`}
                data-part="row"
                data-tone={action.tone}
                role="option"
                aria-selected={current === action}
                onClick={() => setActive(index)}
              >
                {action.label}
                {action.keys ? <kbd>{action.keys}</kbd> : null}
              </li>
            ))}
          </ul>
        )}
      </div>
    </>
  )
}