From 9917bf73cc6b30ac01a18ec02ec93fb372e6b42b Mon Sep 17 00:00:00 2001 From: zeitiv <24879428+zeitiv@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:27:44 +0100 Subject: [PATCH 1/3] feat: add Cmd+K command search to docs website --- apps/showcase/src/app/app.ts | 4 +- .../command-search/command-search.service.ts | 20 ++ .../command-search/command-search.ts | 289 ++++++++++++++++++ .../src/app/components/navbar/navbar.ts | 35 +++ .../app/layouts/docs-layout/docs-layout.ts | 34 ++- 5 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 apps/showcase/src/app/components/command-search/command-search.service.ts create mode 100644 apps/showcase/src/app/components/command-search/command-search.ts diff --git a/apps/showcase/src/app/app.ts b/apps/showcase/src/app/app.ts index db67a0b33..dee40d0d7 100644 --- a/apps/showcase/src/app/app.ts +++ b/apps/showcase/src/app/app.ts @@ -7,12 +7,14 @@ import { } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { cn } from '@semantic-components/ui'; +import { CommandSearch } from './components/command-search/command-search'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, CommandSearch], template: ` + `, host: { '[class]': 'class()', 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..91083b6be --- /dev/null +++ b/apps/showcase/src/app/components/command-search/command-search.ts @@ -0,0 +1,289 @@ +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: ` +
+ + +
+
+
+ + +
+ +
+ @if (!hasResults()) { +
No results found.
+ } + @if (filteredGettingStarted().length > 0) { +
+ Getting Started + @for (item of filteredGettingStarted(); track item.path) { +
+ + {{ item.label }} +
+ } +
+ } + @if (filteredInstallation().length > 0) { +
+
+ Installation + @for (item of filteredInstallation(); track item.path) { +
+ + {{ item.label }} +
+ } +
+ } + @if (filteredComponents().length > 0) { +
+
+ Components + @for (item of filteredComponents(); track item.path) { +
+ + {{ item.label }} +
+ } +
+ } +
+
+
+
+
+
+ `, + 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/apps/showcase/src/app/components/navbar/navbar.ts b/apps/showcase/src/app/components/navbar/navbar.ts index b877f1608..0fdb3f121 100644 --- a/apps/showcase/src/app/components/navbar/navbar.ts +++ b/apps/showcase/src/app/components/navbar/navbar.ts @@ -8,6 +8,8 @@ import { } from '@angular/core'; import { RouterLink, RouterLinkActive } from '@angular/router'; import { + ScButton, + ScKbd, ScLink, ScNavigationMenu, ScNavigationMenuContent, @@ -34,11 +36,13 @@ import { SiGithubIcon, SiMenuIcon, SiMoonIcon, + SiSearchIcon, SiSunIcon, SiXIcon, } from '@semantic-icons/lucide-icons'; import { ComponentsService } from '../../services/components.service'; import { GithubService } from '../../services/github.service'; +import { CommandSearchService } from '../command-search/command-search.service'; import { Logo } from '../logo/logo'; @Component({ @@ -64,7 +68,10 @@ import { Logo } from '../logo/logo'; ScNavigationMenuList, ScNavigationMenuPortal, ScNavigationMenuTrigger, + ScButton, + ScKbd, SiGithubIcon, + SiSearchIcon, SiSunIcon, SiMoonIcon, SiMenuIcon, @@ -139,6 +146,28 @@ import { Logo } from '../logo/logo';
+ + Documentation + +
@@ -342,6 +369,7 @@ export class DocsLayout { protected readonly tocService = inject(TocService); private readonly componentsService = inject(ComponentsService); private readonly config = inject(ConfigService); + private readonly commandSearch = inject(CommandSearchService); private readonly contentArea = viewChild.required>('contentArea'); @@ -349,6 +377,10 @@ export class DocsLayout { readonly components = this.componentsService.visibleComponents; protected readonly devMode = this.config.devMode; + openSearch(): void { + this.commandSearch.open(); + } + constructor() { afterNextRender(() => { this.extractTocHeadings(); From 2c8106a14a83233c8853b5398d40d10732584e51 Mon Sep 17 00:00:00 2001 From: zeitiv <24879428+zeitiv@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:59:39 +0100 Subject: [PATCH 2/3] fix: improve command search dialog responsiveness on mobile --- .../src/app/components/command-search/command-search.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/showcase/src/app/components/command-search/command-search.ts b/apps/showcase/src/app/components/command-search/command-search.ts index 91083b6be..755aa6925 100644 --- a/apps/showcase/src/app/components/command-search/command-search.ts +++ b/apps/showcase/src/app/components/command-search/command-search.ts @@ -69,7 +69,10 @@ interface SearchItem { class="hidden" > -
+
Date: Thu, 12 Mar 2026 01:08:38 +0100 Subject: [PATCH 3/3] fix: change command dialog demo hotkey to Cmd+J to avoid conflict --- .../src/app/pages/docs/command/demos/command-dialog-demo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/showcase/src/app/pages/docs/command/demos/command-dialog-demo.ts b/apps/showcase/src/app/pages/docs/command/demos/command-dialog-demo.ts index 154393cd2..03c584a00 100644 --- a/apps/showcase/src/app/pages/docs/command/demos/command-dialog-demo.ts +++ b/apps/showcase/src/app/pages/docs/command/demos/command-dialog-demo.ts @@ -85,7 +85,7 @@ interface CommandItem { Click or press