Commands
Intro
What Commands are, how to define them, and how to compose them locally.
Tulip Commands are a typed action system for business UIs. You define each command once as a reusable value, then compose the commands you need where they are rendered.
Entry points
import { commandBuilder, createCommandBuilder, ensureArray } from "@tulip-systems/commands";
import { InlineCommandMenu, DeleteCommand } from "@tulip-systems/commands/client";@tulip-systems/commands: command definitions and helpers@tulip-systems/commands/client: menu components, command primitives, and hooks
Recommended mental model
- Atomic command: one exported command value such as
projectArchiveCommand - Local composition: pass an inline array of command values to the menu where they are used
- Shared normalization: use
.transform(ensureArray)when one command must support single-item and multi-item inputs
Why this pattern
This keeps the system explicit and boring:
- action behavior stays centralized in the command definition
- each screen still clearly shows which commands it renders
- no tag systems, registries, or query-style command selection APIs are needed
- permissions, visibility, disabled state, and mutation side-effects stay with the command itself
First command
import { commandBuilder } from "@tulip-systems/commands";
import { CommandClick, CommandLabel, InlineCommandMenu } from "@tulip-systems/commands/client";
import { ArchiveIcon } from "lucide-react";
import { orpc } from "@/server/router/client";
type ProjectData = { id: string };
export const projectArchiveCommand = commandBuilder
.$type<ProjectData>()
.permission({ project: ["archive"] })
.render(({ data }) => (
<CommandClick
label="Archive"
variables={{ ids: [data.id] }}
mutation={orpc.projects.archive.mutationOptions()}
>
<ArchiveIcon className="w-4" />
<CommandLabel />
</CommandClick>
));
export function ProjectActions({ project }: { project: ProjectData }) {
return <InlineCommandMenu data={project} commands={[projectArchiveCommand]} />;
}Local composition
Compose command arrays where the UI renders them:
<ResponsiveCommandMenu
data={project}
commands={[
projectUpdateStatusCommand,
projectArchiveCommand,
projectRestoreCommand,
projectDeleteCommand,
]}
/>For a create action in a toolbar:
<InlineCommandMenu data={null} commands={[projectCreateCommand]} />If the same array is reused often, you can extract a nearby local constant, but avoid vague exported groups like singleCommands, bulkCommands, or globalCommands.
Common helpers
commandBuilder: define a command with typed input datacreateCommandBuilder<TMeta>(): define commands that also need sharedmetaensureArray: normalizeT | T[]intoT[]- menu components:
InlineCommandMenu,DropdownCommandMenu,ContextCommandMenuContent,FloatingCommandMenu,ResponsiveCommandMenu - utility primitives:
DeleteCommand,ArchiveCommand,RestoreCommand,CommandClick,CommandDialog,CommandFormDialog
When a command does not render
Check these in order:
- the menu received an empty
commandsarray .visibleWhen(...)returnedfalseor an array containingfalse- the current user does not satisfy
.permission(...) - the command rendered but is disabled by
.disabledWhen(...)