Run the CLI
Use the CLI to add the component to your project.
npx @ngzard/ui@latest add commandFast, composable, styled command menu for Angular.
import { Component } from '@angular/core';
import type { ZardCommandOption } from '../command.component';
import { ZardCommandModule } from '../command.module';
@Component({
selector: 'z-demo-command-default',
imports: [ZardCommandModule],
standalone: true,
template: `
<z-command class="md:min-w-[500px]" (zCommandSelected)="handleCommand($event)">
<z-command-input placeholder="Search actions, files, and more..." />
<z-command-list>
<z-command-empty>No commands found.</z-command-empty>
<z-command-option-group zLabel="Quick Actions">
<z-command-option zLabel="Create new project" zValue="new-project" zIcon="folder" zShortcut="⌘N" />
<z-command-option zLabel="Open file" zValue="open-file" zIcon="folder-open" zShortcut="⌘O" />
<z-command-option zLabel="Save all" zValue="save-all" zIcon="save" zShortcut="⌘S" />
</z-command-option-group>
<z-command-divider />
<z-command-option-group zLabel="Navigation">
<z-command-option zLabel="Go to Dashboard" zValue="dashboard" zIcon="layout-dashboard" zShortcut="⌘1" />
<z-command-option zLabel="Go to Projects" zValue="projects" zIcon="folder" zShortcut="⌘2" />
</z-command-option-group>
<z-command-divider />
<z-command-option-group zLabel="Tools">
<z-command-option zLabel="Open terminal" zValue="terminal" zIcon="terminal" zShortcut="⌘T" />
<z-command-option zLabel="Toggle theme" zValue="theme" zIcon="moon" zShortcut="⌘D" />
</z-command-option-group>
</z-command-list>
</z-command>
`,
host: {
'(window:keydown)': 'handleKeydown($event)',
},
})
export class ZardDemoCommandDefaultComponent {
// Handle command selection
handleCommand(option: ZardCommandOption) {
const action = `Executed "${option.label}" (value: ${option.value})`;
console.log(action);
// You can add real logic here
switch (option.value) {
case 'new-project':
this.showAlert('Creating new project...');
break;
case 'open-file':
this.showAlert('Opening file dialog...');
break;
case 'save-all':
this.showAlert('Saving all files...');
break;
case 'dashboard':
this.showAlert('Navigating to Dashboard...');
break;
case 'projects':
this.showAlert('Navigating to Projects...');
break;
case 'terminal':
this.showAlert('Opening terminal...');
break;
case 'theme':
this.showAlert('Toggling theme...');
break;
default:
this.showAlert(`Action: ${option.label}`);
}
}
// Handle keyboard shortcuts
handleKeydown(event: KeyboardEvent) {
if (event.metaKey || event.ctrlKey) {
if ('nos12td'.includes(event.key.toLowerCase())) {
event.preventDefault();
}
switch (event.key.toLowerCase()) {
case 'n':
this.executeCommand('new-project', 'Create new project');
break;
case 'o':
this.executeCommand('open-file', 'Open file');
break;
case 's':
this.executeCommand('save-all', 'Save all');
break;
case '1':
this.executeCommand('dashboard', 'Go to Dashboard');
break;
case '2':
this.executeCommand('projects', 'Go to Projects');
break;
case 't':
this.executeCommand('terminal', 'Open terminal');
break;
case 'd':
this.executeCommand('theme', 'Toggle theme');
break;
}
}
}
private executeCommand(value: string, label: string) {
this.handleCommand({ value, label } as ZardCommandOption);
}
private showAlert(message: string, isWarning = false) {
if (isWarning) {
console.warn(message);
} else {
console.log(message);
}
// In a real app, you might show a toast notification here
setTimeout(() => {
// You could clear the action after some time
}, 3000);
}
}
Use the CLI to add the component to your project.
npx @ngzard/ui@latest add commandpnpm dlx @ngzard/ui@latest add commandyarn dlx @ngzard/ui@latest add commandbunx @ngzard/ui@latest add commandCreate the component directory structure and add the following files to your project.
import {
ChangeDetectionStrategy,
Component,
computed,
contentChild,
contentChildren,
effect,
forwardRef,
input,
output,
signal,
ViewEncapsulation,
} from '@angular/core';
import { type ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import type { ClassValue } from 'clsx';
import { ZardCommandInputComponent } from './command-input.component';
import { ZardCommandOptionComponent } from './command-option.component';
import { commandVariants, type ZardCommandVariants } from './command.variants';
import { mergeClasses } from '../../shared/utils/utils';
import type { ZardIcon } from '../icon/icons';
export interface ZardCommandOption {
value: unknown;
label: string;
disabled?: boolean;
command?: string;
shortcut?: string;
icon?: ZardIcon;
action?: () => void;
key?: string; // Keyboard shortcut key (e.g., 'n' for Ctrl+N)
}
export interface ZardCommandGroup {
label: string;
options: ZardCommandOption[];
}
export interface ZardCommandConfig {
placeholder?: string;
emptyText?: string;
groups: ZardCommandGroup[];
dividers?: boolean;
onSelect?: (option: ZardCommandOption) => void;
}
@Component({
selector: 'z-command',
imports: [FormsModule],
template: `
<div [class]="classes()">
<div id="command-instructions" class="sr-only">
Use arrow keys to navigate, Enter to select, Escape to clear selection.
</div>
<div id="command-status" class="sr-only" aria-live="polite" aria-atomic="true">
{{ statusMessage() }}
</div>
<ng-content />
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ZardCommandComponent),
multi: true,
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {
role: 'combobox',
'aria-haspopup': 'listbox',
'[attr.aria-expanded]': 'true',
'(keydown.{arrowdown,arrowup,enter,escape}.prevent)': 'onKeyDown($event)',
},
exportAs: 'zCommand',
})
export class ZardCommandComponent implements ControlValueAccessor {
readonly commandInput = contentChild(ZardCommandInputComponent);
readonly optionComponents = contentChildren(ZardCommandOptionComponent, { descendants: true });
readonly size = input<ZardCommandVariants['size']>('default');
readonly class = input<ClassValue>('');
readonly zCommandChange = output<ZardCommandOption>();
readonly zCommandSelected = output<ZardCommandOption>();
// Internal signals for search functionality
readonly searchTerm = signal('');
readonly selectedIndex = signal(-1);
// Signal to trigger updates when optionComponents change
private readonly optionsUpdateTrigger = signal(0);
protected readonly classes = computed(() => mergeClasses(commandVariants({ size: this.size() }), this.class()));
// Computed signal for filtered options - this will automatically update when searchTerm or options change
readonly filteredOptions = computed(() => {
const searchTerm = this.searchTerm();
// Include the trigger signal to make this computed reactive to option changes
this.optionsUpdateTrigger();
if (!this.optionComponents()) {
return [];
}
const lowerSearchTerm = searchTerm.toLowerCase().trim();
if (!lowerSearchTerm) {
return this.optionComponents();
}
return this.optionComponents().filter(option => {
const label = option.zLabel().toLowerCase();
const command = option.zCommand()?.toLowerCase() ?? '';
return label.includes(lowerSearchTerm) || command.includes(lowerSearchTerm);
});
});
// Status message for screen readers
protected readonly statusMessage = computed(() => {
const searchTerm = this.searchTerm().trim();
const filteredCount = this.filteredOptions().length;
if (!searchTerm) {
return searchTerm;
}
if (!filteredCount) {
return `No results found for "${searchTerm}"`;
}
return `${filteredCount} result${filteredCount === 1 ? '' : 's'} found for "${searchTerm}"`;
});
private onChange = (_value: unknown) => {
// ControlValueAccessor implementation
};
private onTouched = () => {
// ControlValueAccessor implementation
};
constructor() {
effect(() => {
this.triggerOptionsUpdate();
});
}
/**
* Trigger an update to the filteredOptions computed signal
*/
private triggerOptionsUpdate(): void {
this.optionsUpdateTrigger.update(value => value + 1);
}
onSearch(searchTerm: string) {
this.searchTerm.set(searchTerm);
this.selectedIndex.set(-1);
this.updateSelectedOption();
}
selectOption(option: ZardCommandOptionComponent) {
const commandOption: ZardCommandOption = {
value: option.zValue(),
label: option.zLabel(),
disabled: option.zDisabled(),
command: option.zCommand(),
shortcut: option.zShortcut(),
icon: option.zIcon(),
};
this.onChange(commandOption.value);
this.zCommandChange.emit(commandOption);
this.zCommandSelected.emit(commandOption);
}
// in @Component host: '(keydown)': 'onKeyDown($event)'
onKeyDown(event: Event) {
const filteredOptions = this.filteredOptions();
if (filteredOptions.length === 0) {
return;
}
const { key } = event as KeyboardEvent;
const currentIndex = this.selectedIndex();
switch (key) {
case 'ArrowDown': {
const nextIndex = currentIndex < filteredOptions.length - 1 ? currentIndex + 1 : 0;
this.selectedIndex.set(nextIndex);
this.updateSelectedOption();
break;
}
case 'ArrowUp': {
const prevIndex = currentIndex > 0 ? currentIndex - 1 : filteredOptions.length - 1;
this.selectedIndex.set(prevIndex);
this.updateSelectedOption();
break;
}
case 'Enter':
if (currentIndex >= 0 && currentIndex < filteredOptions.length) {
const selectedOption = filteredOptions[currentIndex];
if (!selectedOption.zDisabled()) {
this.selectOption(selectedOption);
}
}
break;
case 'Escape':
this.selectedIndex.set(-1);
this.updateSelectedOption();
break;
}
}
private updateSelectedOption() {
const filteredOptions = this.filteredOptions();
const selectedIndex = this.selectedIndex();
// Clear previous selection
for (const option of filteredOptions) {
option.setSelected(false);
}
// Set new selection
if (selectedIndex >= 0 && selectedIndex < filteredOptions.length) {
const selectedOption = filteredOptions[selectedIndex];
selectedOption.setSelected(true);
selectedOption.focus();
}
}
// ControlValueAccessor implementation
writeValue(_value: unknown): void {
// Implementation if needed for form control integration
}
registerOnChange(fn: (value: unknown) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(_isDisabled: boolean): void {
// Implementation if needed for form control disabled state
}
/**
* Refresh the options list - useful when options are added/removed dynamically
*/
refreshOptions(): void {
this.triggerOptionsUpdate();
}
/**
* Focus the command input
*/
focus(): void {
this.commandInput()?.focus();
}
}
import { cva, type VariantProps } from 'class-variance-authority';
export const commandVariants = cva(
'flex h-full w-full flex-col overflow-hidden shadow-md border rounded-md bg-popover text-popover-foreground',
{
variants: {
size: {
sm: 'min-h-64',
default: 'min-h-80',
lg: 'min-h-96',
xl: 'min-h-[30rem]',
},
},
defaultVariants: {
size: 'default',
},
},
);
export const commandInputVariants = cva(
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
{
variants: {},
defaultVariants: {},
},
);
export const commandListVariants = cva('max-h-[300px] overflow-y-auto overflow-x-hidden p-1', {
variants: {},
defaultVariants: {},
});
export const commandEmptyVariants = cva('py-6 text-center text-sm text-muted-foreground', {
variants: {},
defaultVariants: {},
});
export const commandGroupVariants = cva('overflow-hidden text-foreground', {
variants: {},
defaultVariants: {},
});
export const commandGroupHeadingVariants = cva('px-2 py-1.5 text-xs font-medium text-muted-foreground', {
variants: {},
defaultVariants: {},
});
export const commandItemVariants = cva(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
{
variants: {
variant: {
default: '',
destructive:
'aria-selected:bg-destructive aria-selected:text-destructive-foreground hover:bg-destructive hover:text-destructive-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
export const commandSeparatorVariants = cva('-mx-1 my-1 h-px bg-border', {
variants: {},
defaultVariants: {},
});
export const commandShortcutVariants = cva('ml-auto text-xs tracking-widest text-muted-foreground', {
variants: {},
defaultVariants: {},
});
export type ZardCommandVariants = VariantProps<typeof commandVariants>;
export type ZardCommandItemVariants = VariantProps<typeof commandItemVariants>;
import { ChangeDetectionStrategy, Component, computed, inject, input, ViewEncapsulation } from '@angular/core';
import type { ClassValue } from 'clsx';
import { ZardCommandComponent } from './command.component';
import { commandSeparatorVariants } from './command.variants';
import { mergeClasses } from '../../shared/utils/utils';
@Component({
selector: 'z-command-divider',
template: `
@if (shouldShow()) {
<div [class]="classes()" role="separator"></div>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'zCommandDivider',
})
export class ZardCommandDividerComponent {
private readonly commandComponent = inject(ZardCommandComponent, { optional: true });
readonly class = input<ClassValue>('');
protected readonly classes = computed(() => mergeClasses(commandSeparatorVariants({}), this.class()));
protected readonly shouldShow = computed(() => {
if (!this.commandComponent) {
return true;
}
const searchTerm = this.commandComponent.searchTerm();
// If no search, always show dividers
if (searchTerm === '') {
return true;
}
// If there's a search term, hide all dividers for now
// This is a simple approach - we can make it smarter later
return false;
});
}
import { ChangeDetectionStrategy, Component, computed, inject, input, ViewEncapsulation } from '@angular/core';
import type { ClassValue } from 'clsx';
import { ZardCommandComponent } from './command.component';
import { commandEmptyVariants } from './command.variants';
import { mergeClasses } from '../../shared/utils/utils';
@Component({
selector: 'z-command-empty',
template: `
@if (shouldShow()) {
<div [class]="classes()">
<ng-content>No results found.</ng-content>
</div>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'zCommandEmpty',
})
export class ZardCommandEmptyComponent {
private readonly commandComponent = inject(ZardCommandComponent, { optional: true });
readonly class = input<ClassValue>('');
protected readonly classes = computed(() => mergeClasses(commandEmptyVariants({}), this.class()));
protected readonly shouldShow = computed(() => {
// Check traditional command component
if (this.commandComponent) {
const filteredOptions = this.commandComponent.filteredOptions();
return filteredOptions.length === 0;
}
return false;
});
}
import {
ChangeDetectionStrategy,
Component,
computed,
type ElementRef,
forwardRef,
inject,
input,
output,
signal,
viewChild,
ViewEncapsulation,
} from '@angular/core';
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import type { ClassValue } from 'clsx';
import { ZardCommandComponent } from './command.component';
import { commandInputVariants } from './command.variants';
import { mergeClasses } from '../../shared/utils/utils';
import { checkForProperZardInitialization } from '../core/provider/providezard';
import { ZardIconComponent } from '../icon/icon.component';
@Component({
selector: 'z-command-input',
imports: [ZardIconComponent],
template: `
<div class="flex items-center border-b px-3" cmdk-input-wrapper="">
<z-icon zType="search" class="mr-2 shrink-0 opacity-50" />
<input
#searchInput
[class]="classes()"
[placeholder]="placeholder()"
[value]="searchTerm()"
[disabled]="disabled()"
(input.debounce.150)="onInput($event)"
(keydown)="onKeyDown($event)"
(blur)="onTouched()"
aria-controls="command-list"
aria-describedby="command-instructions"
aria-haspopup="listbox"
aria-label="Search commands"
autocomplete="off"
autocorrect="off"
spellcheck="false"
role="combobox"
[attr.aria-expanded]="true"
/>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ZardCommandInputComponent),
multi: true,
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'zCommandInput',
})
export class ZardCommandInputComponent implements ControlValueAccessor {
private readonly commandComponent = inject(ZardCommandComponent, { optional: true });
readonly searchInput = viewChild.required<ElementRef<HTMLInputElement>>('searchInput');
readonly placeholder = input<string>('Type a command or search...');
readonly class = input<ClassValue>('');
readonly valueChange = output<string>();
readonly searchTerm = signal('');
readonly classes = computed(() => mergeClasses(commandInputVariants({}), this.class()));
readonly disabled = signal(false);
protected onChange = (_: string) => {
// ControlValueAccessor implementation - intentionally empty
};
protected onTouched = () => {
// ControlValueAccessor implementation - intentionally empty
};
constructor() {
checkForProperZardInitialization();
}
onInput(event: Event) {
const target = event.target as HTMLInputElement;
const { value } = target;
this.searchTerm.set(value);
this.updateParentComponents(value);
}
updateParentComponents(value: string): void {
// Send search to appropriate parent component
if (this.commandComponent) {
this.commandComponent.onSearch(value);
}
this.onChange(value);
this.valueChange.emit(value);
}
onKeyDown(event: KeyboardEvent) {
// Let parent command component handle navigation keys
if (['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(event.key)) {
// For Escape key, don't stop propagation to allow document listener to work
if (event.key !== 'Escape') {
event.preventDefault(); // Prevent default input behavior
event.stopPropagation(); // Stop the event from bubbling up
}
// Send to parent command component
if (this.commandComponent) {
this.commandComponent.onKeyDown(event);
}
}
// Handle other keys as needed
}
writeValue(value: string | null): void {
const normalizedValue = value ?? '';
this.searchTerm.set(normalizedValue);
if (this.commandComponent) {
this.commandComponent.onSearch(normalizedValue);
}
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled.set(isDisabled);
}
/**
* Focus the input element
*/
focus(): void {
this.searchInput().nativeElement.focus();
}
}
import { ChangeDetectionStrategy, Component, computed, input, ViewEncapsulation } from '@angular/core';
import type { ClassValue } from 'clsx';
import { commandListVariants } from './command.variants';
import { mergeClasses } from '../../shared/utils/utils';
@Component({
selector: 'z-command-list',
standalone: true,
template: `
<div [class]="classes()" role="listbox" id="command-list">
<ng-content />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'zCommandList',
})
export class ZardCommandListComponent {
readonly class = input<ClassValue>('');
protected readonly classes = computed(() => mergeClasses(commandListVariants({}), this.class()));
}
import {
ChangeDetectionStrategy,
Component,
computed,
contentChildren,
inject,
input,
ViewEncapsulation,
} from '@angular/core';
import type { ClassValue } from 'clsx';
import { ZardCommandOptionComponent } from './command-option.component';
import { ZardCommandComponent } from './command.component';
import { commandGroupHeadingVariants, commandGroupVariants } from './command.variants';
import { mergeClasses } from '../../shared/utils/utils';
@Component({
selector: 'z-command-option-group',
standalone: true,
template: `
@if (shouldShow()) {
<div [class]="classes()" role="group">
@if (zLabel()) {
<div [class]="headingClasses()" role="presentation">
{{ zLabel() }}
</div>
}
<div role="group">
<ng-content />
</div>
</div>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'zCommandOptionGroup',
})
export class ZardCommandOptionGroupComponent {
private readonly commandComponent = inject(ZardCommandComponent, { optional: true });
readonly optionComponents = contentChildren(ZardCommandOptionComponent, { descendants: true });
readonly zLabel = input.required<string>();
readonly class = input<ClassValue>('');
protected readonly classes = computed(() => mergeClasses(commandGroupVariants({}), this.class()));
protected readonly headingClasses = computed(() => mergeClasses(commandGroupHeadingVariants({})));
protected readonly shouldShow = computed(() => {
if (!this.commandComponent || !this.optionComponents().length) {
return true;
}
const searchTerm = this.commandComponent.searchTerm();
// If no search term, show all groups
if (searchTerm === '') {
return true;
}
const filteredOptions = this.commandComponent.filteredOptions();
// Check if any option in this group is in the filtered list
return this.optionComponents().some(option => filteredOptions.includes(option));
});
}
import {
ChangeDetectionStrategy,
Component,
computed,
ElementRef,
inject,
input,
signal,
ViewEncapsulation,
} from '@angular/core';
import type { ClassValue } from 'clsx';
import { ZardCommandComponent } from './command.component';
import { commandItemVariants, commandShortcutVariants, type ZardCommandItemVariants } from './command.variants';
import { mergeClasses, transform } from '../../shared/utils/utils';
import { ZardIconComponent } from '../icon/icon.component';
import type { ZardIcon } from '../icon/icons';
@Component({
selector: 'z-command-option',
imports: [ZardIconComponent],
template: `
@if (shouldShow()) {
<div
[class]="classes()"
[attr.role]="'option'"
[attr.aria-selected]="isSelected()"
[attr.data-selected]="isSelected()"
[attr.data-disabled]="zDisabled()"
[attr.tabindex]="0"
(click)="onClick()"
(keydown.{enter,space}.prevent)="onClick()"
(mouseenter)="onMouseEnter()"
>
@if (zIcon()) {
<div z-icon [zType]="zIcon()!" class="mr-2 flex shrink-0 items-center justify-center"></div>
}
<span class="flex-1">{{ zLabel() }}</span>
@if (zShortcut()) {
<span [class]="shortcutClasses()">{{ zShortcut() }}</span>
}
</div>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'zCommandOption',
})
export class ZardCommandOptionComponent {
private readonly elementRef = inject(ElementRef);
private readonly commandComponent = inject(ZardCommandComponent, { optional: true });
readonly zValue = input.required<unknown>();
readonly zLabel = input.required<string>();
readonly zCommand = input<string>('');
readonly zIcon = input<ZardIcon>();
readonly zShortcut = input<string>('');
readonly zDisabled = input(false, { transform });
readonly variant = input<ZardCommandItemVariants['variant']>('default');
readonly class = input<ClassValue>('');
readonly isSelected = signal(false);
protected readonly classes = computed(() => {
const baseClasses = commandItemVariants({ variant: this.variant() });
const selectedClasses = this.isSelected() ? 'bg-accent text-accent-foreground' : '';
return mergeClasses(baseClasses, selectedClasses, this.class());
});
protected readonly shortcutClasses = computed(() => mergeClasses(commandShortcutVariants({})));
protected readonly shouldShow = computed(() => {
if (!this.commandComponent) {
return true;
}
const filteredOptions = this.commandComponent.filteredOptions();
const searchTerm = this.commandComponent.searchTerm();
// If no search term, show all options
if (searchTerm === '') {
return true;
}
// Check if this option is in the filtered list
return filteredOptions.includes(this);
});
onClick() {
if (this.zDisabled()) {
return;
}
if (this.commandComponent) {
this.commandComponent.selectOption(this);
}
}
onMouseEnter() {
if (this.zDisabled()) {
return;
}
// Visual feedback for hover
}
setSelected(selected: boolean) {
this.isSelected.set(selected);
}
focus() {
const element = this.elementRef.nativeElement;
element.focus();
// Scroll element into view if needed
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ZardCommandDividerComponent } from './command-divider.component';
import { ZardCommandEmptyComponent } from './command-empty.component';
import { ZardCommandInputComponent } from './command-input.component';
import { ZardCommandListComponent } from './command-list.component';
import { ZardCommandOptionGroupComponent } from './command-option-group.component';
import { ZardCommandOptionComponent } from './command-option.component';
import { ZardCommandComponent } from './command.component';
const COMMAND_COMPONENTS = [
ZardCommandComponent,
ZardCommandInputComponent,
ZardCommandListComponent,
ZardCommandEmptyComponent,
ZardCommandOptionComponent,
ZardCommandOptionGroupComponent,
ZardCommandDividerComponent,
];
@NgModule({
imports: [FormsModule, ...COMMAND_COMPONENTS],
exports: [...COMMAND_COMPONENTS],
})
export class ZardCommandModule {}
import { Component } from '@angular/core';
import type { ZardCommandOption } from '../command.component';
import { ZardCommandModule } from '../command.module';
@Component({
selector: 'z-demo-command-default',
imports: [ZardCommandModule],
standalone: true,
template: `
<z-command class="md:min-w-[500px]" (zCommandSelected)="handleCommand($event)">
<z-command-input placeholder="Search actions, files, and more..." />
<z-command-list>
<z-command-empty>No commands found.</z-command-empty>
<z-command-option-group zLabel="Quick Actions">
<z-command-option zLabel="Create new project" zValue="new-project" zIcon="folder" zShortcut="⌘N" />
<z-command-option zLabel="Open file" zValue="open-file" zIcon="folder-open" zShortcut="⌘O" />
<z-command-option zLabel="Save all" zValue="save-all" zIcon="save" zShortcut="⌘S" />
</z-command-option-group>
<z-command-divider />
<z-command-option-group zLabel="Navigation">
<z-command-option zLabel="Go to Dashboard" zValue="dashboard" zIcon="layout-dashboard" zShortcut="⌘1" />
<z-command-option zLabel="Go to Projects" zValue="projects" zIcon="folder" zShortcut="⌘2" />
</z-command-option-group>
<z-command-divider />
<z-command-option-group zLabel="Tools">
<z-command-option zLabel="Open terminal" zValue="terminal" zIcon="terminal" zShortcut="⌘T" />
<z-command-option zLabel="Toggle theme" zValue="theme" zIcon="moon" zShortcut="⌘D" />
</z-command-option-group>
</z-command-list>
</z-command>
`,
host: {
'(window:keydown)': 'handleKeydown($event)',
},
})
export class ZardDemoCommandDefaultComponent {
// Handle command selection
handleCommand(option: ZardCommandOption) {
const action = `Executed "${option.label}" (value: ${option.value})`;
console.log(action);
// You can add real logic here
switch (option.value) {
case 'new-project':
this.showAlert('Creating new project...');
break;
case 'open-file':
this.showAlert('Opening file dialog...');
break;
case 'save-all':
this.showAlert('Saving all files...');
break;
case 'dashboard':
this.showAlert('Navigating to Dashboard...');
break;
case 'projects':
this.showAlert('Navigating to Projects...');
break;
case 'terminal':
this.showAlert('Opening terminal...');
break;
case 'theme':
this.showAlert('Toggling theme...');
break;
default:
this.showAlert(`Action: ${option.label}`);
}
}
// Handle keyboard shortcuts
handleKeydown(event: KeyboardEvent) {
if (event.metaKey || event.ctrlKey) {
if ('nos12td'.includes(event.key.toLowerCase())) {
event.preventDefault();
}
switch (event.key.toLowerCase()) {
case 'n':
this.executeCommand('new-project', 'Create new project');
break;
case 'o':
this.executeCommand('open-file', 'Open file');
break;
case 's':
this.executeCommand('save-all', 'Save all');
break;
case '1':
this.executeCommand('dashboard', 'Go to Dashboard');
break;
case '2':
this.executeCommand('projects', 'Go to Projects');
break;
case 't':
this.executeCommand('terminal', 'Open terminal');
break;
case 'd':
this.executeCommand('theme', 'Toggle theme');
break;
}
}
}
private executeCommand(value: string, label: string) {
this.handleCommand({ value, label } as ZardCommandOption);
}
private showAlert(message: string, isWarning = false) {
if (isWarning) {
console.warn(message);
} else {
console.log(message);
}
// In a real app, you might show a toast notification here
setTimeout(() => {
// You could clear the action after some time
}, 3000);
}
}