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: ` +
+ + +
+
+
+ + +
+ +
+ @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/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);