diff --git a/apps/showcase/src/app/components/command-search/command-search.service.ts b/apps/showcase/src/app/components/command-search/command-search.service.ts
new file mode 100644
index 000000000..9bab81fd1
--- /dev/null
+++ b/apps/showcase/src/app/components/command-search/command-search.service.ts
@@ -0,0 +1,20 @@
+import { Injectable, signal } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class CommandSearchService {
+ readonly isOpen = signal(false);
+
+ open(): void {
+ this.isOpen.set(true);
+ }
+
+ close(): void {
+ this.isOpen.set(false);
+ }
+
+ toggle(): void {
+ this.isOpen.update((v) => !v);
+ }
+}
diff --git a/apps/showcase/src/app/components/command-search/command-search.ts b/apps/showcase/src/app/components/command-search/command-search.ts
new file mode 100644
index 000000000..755aa6925
--- /dev/null
+++ b/apps/showcase/src/app/components/command-search/command-search.ts
@@ -0,0 +1,292 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ ViewEncapsulation,
+ computed,
+ inject,
+ signal,
+} from '@angular/core';
+import { Router } from '@angular/router';
+import {
+ ScCommand,
+ ScCommandEmpty,
+ ScCommandGroup,
+ ScCommandGroupLabel,
+ ScCommandInput,
+ ScCommandInputGroup,
+ ScCommandItem,
+ ScCommandList,
+ ScCommandListContainer,
+ ScCommandSeparator,
+ ScDialog,
+ ScDialogPortal,
+ ScDialogProvider,
+ ScHotkey,
+} from '@semantic-components/ui';
+import {
+ SiBoxIcon,
+ SiDownloadIcon,
+ SiFileTextIcon,
+ SiSearchIcon,
+} from '@semantic-icons/lucide-icons';
+import { ComponentsService } from '../../services/components.service';
+import { CommandSearchService } from './command-search.service';
+
+interface SearchItem {
+ label: string;
+ path: string;
+ group: 'getting-started' | 'installation' | 'component';
+ keywords?: string[];
+}
+
+@Component({
+ selector: 'app-command-search',
+ imports: [
+ ScCommand,
+ ScCommandEmpty,
+ ScCommandGroup,
+ ScCommandGroupLabel,
+ ScCommandInput,
+ ScCommandInputGroup,
+ ScCommandItem,
+ ScCommandList,
+ ScCommandListContainer,
+ ScCommandSeparator,
+ ScDialog,
+ ScDialogPortal,
+ ScDialogProvider,
+ ScHotkey,
+ SiBoxIcon,
+ SiDownloadIcon,
+ SiFileTextIcon,
+ SiSearchIcon,
+ ],
+ template: `
+
+ `,
+ host: { class: 'block' },
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CommandSearch {
+ private readonly router = inject(Router);
+ private readonly componentsService = inject(ComponentsService);
+ protected readonly commandSearchService = inject(CommandSearchService);
+
+ readonly searchString = signal('');
+
+ private readonly gettingStarted: SearchItem[] = [
+ {
+ label: 'Introduction',
+ path: '/docs/getting-started/introduction',
+ group: 'getting-started',
+ keywords: ['getting started', 'overview'],
+ },
+ {
+ label: 'Components',
+ path: '/docs/components',
+ group: 'getting-started',
+ keywords: ['list', 'overview'],
+ },
+ ];
+
+ private readonly installation: SearchItem[] = [
+ {
+ label: 'Prerequisites',
+ path: '/docs/getting-started/prerequisites',
+ group: 'installation',
+ keywords: ['install', 'setup', 'requirements'],
+ },
+ {
+ label: 'UI',
+ path: '/docs/getting-started/ui',
+ group: 'installation',
+ keywords: ['core', 'install'],
+ },
+ {
+ label: 'UI Lab',
+ path: '/docs/getting-started/ui-lab',
+ group: 'installation',
+ keywords: ['experimental', 'install'],
+ },
+ {
+ label: 'Carousel',
+ path: '/docs/getting-started/carousel',
+ group: 'installation',
+ keywords: ['slider', 'install'],
+ },
+ {
+ label: 'Charts',
+ path: '/docs/getting-started/charts',
+ group: 'installation',
+ keywords: ['graph', 'install'],
+ },
+ {
+ label: 'Editor',
+ path: '/docs/getting-started/editor',
+ group: 'installation',
+ keywords: ['rich text', 'install'],
+ },
+ {
+ label: 'Code',
+ path: '/docs/getting-started/code',
+ group: 'installation',
+ keywords: ['highlight', 'syntax', 'install'],
+ },
+ {
+ label: 'MCP Server',
+ path: '/docs/getting-started/mcp-server',
+ group: 'installation',
+ keywords: ['model context protocol', 'ai'],
+ },
+ ];
+
+ private readonly componentItems = computed(() =>
+ this.componentsService.visibleComponents().map((c) => ({
+ label: c.name,
+ path: `/docs/components/${c.path}`,
+ group: 'component' as const,
+ keywords: [c.category.toLowerCase(), c.description.toLowerCase()],
+ })),
+ );
+
+ readonly filteredGettingStarted = computed(() => {
+ const search = this.searchString().toLowerCase();
+ return this.filterItems(this.gettingStarted, search);
+ });
+
+ readonly filteredInstallation = computed(() => {
+ const search = this.searchString().toLowerCase();
+ return this.filterItems(this.installation, search);
+ });
+
+ readonly filteredComponents = computed(() => {
+ const search = this.searchString().toLowerCase();
+ return this.filterItems(this.componentItems(), search);
+ });
+
+ readonly hasResults = computed(
+ () =>
+ this.filteredGettingStarted().length > 0 ||
+ this.filteredInstallation().length > 0 ||
+ this.filteredComponents().length > 0,
+ );
+
+ private filterItems(items: SearchItem[], search: string): SearchItem[] {
+ if (!search) return items;
+ return items.filter(
+ (item) =>
+ item.label.toLowerCase().includes(search) ||
+ item.keywords?.some((k) => k.includes(search)),
+ );
+ }
+
+ onSelect(values: readonly string[]): void {
+ const path = values[0];
+ if (path) {
+ this.navigate(path);
+ }
+ }
+
+ navigate(path: string): void {
+ this.commandSearchService.close();
+ this.searchString.set('');
+ this.router.navigateByUrl(path);
+ }
+}
diff --git a/libs/ui/src/lib/components/scroll-area/scroll-area-scrollbar.ts b/libs/ui/src/lib/components/scroll-area/scroll-area-scrollbar.ts
index 38de2ef64..6615ff960 100644
--- a/libs/ui/src/lib/components/scroll-area/scroll-area-scrollbar.ts
+++ b/libs/ui/src/lib/components/scroll-area/scroll-area-scrollbar.ts
@@ -198,9 +198,6 @@ export class ScScrollBar {
);
protected readonly thumbClass = computed(() =>
- cn(
- 'rounded-full bg-border',
- this.isVertical() ? 'w-full' : 'h-full',
- ),
+ cn('rounded-full bg-border', this.isVertical() ? 'w-full' : 'h-full'),
);
}
diff --git a/libs/ui/src/lib/components/scroll-area/scroll-area.ts b/libs/ui/src/lib/components/scroll-area/scroll-area.ts
index bed416790..247393b75 100644
--- a/libs/ui/src/lib/components/scroll-area/scroll-area.ts
+++ b/libs/ui/src/lib/components/scroll-area/scroll-area.ts
@@ -47,8 +47,7 @@ import { SC_SCROLL_AREA, type ScScrollAreaContext } from './scroll-area-types';
export class ScScrollArea implements ScScrollAreaContext {
readonly classInput = input('', { alias: 'class' });
- private readonly viewportRef =
- viewChild>('viewport');
+ private readonly viewportRef = viewChild>('viewport');
readonly viewport = computed(() => this.viewportRef()?.nativeElement);