chore: update Angular TypeScript build info cache

This commit is contained in:
2025-10-02 10:55:11 -04:00
parent 1d4da38164
commit 20cc6e9215
59 changed files with 8614 additions and 496 deletions
@@ -0,0 +1,349 @@
import {
Component,
Input,
Output,
EventEmitter,
signal,
computed,
ChangeDetectionStrategy,
inject
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SearchResult, SearchMatch } from '../../core/search/search-evaluator.service';
import { VaultService } from '../../services/vault.service';
/**
* Group of search results by file
*/
interface ResultGroup {
noteId: string;
fileName: string;
filePath: string;
matches: SearchMatch[];
matchCount: number;
isExpanded: boolean;
score: number;
}
/**
* Sort option for results
*/
type SortOption = 'relevance' | 'name' | 'modified';
/**
* Search results component
* Displays search results grouped by file with highlighting and navigation
*/
@Component({
selector: 'app-search-results',
standalone: true,
imports: [CommonModule, FormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col h-full bg-bg-primary dark:bg-gray-900">
<!-- Results header -->
<div class="flex items-center justify-between p-4 border-b border-border dark:border-gray-700">
<div class="flex items-center gap-3">
<h3 class="text-sm font-semibold text-text-main dark:text-gray-100">
{{ totalResults() }} {{ totalResults() === 1 ? 'result' : 'results' }}
@if (totalMatches() > 0) {
<span class="text-text-muted dark:text-gray-400">
({{ totalMatches() }} {{ totalMatches() === 1 ? 'match' : 'matches' }})
</span>
}
</h3>
</div>
<!-- Sort options -->
<div class="flex items-center gap-2">
<label class="text-xs text-text-muted dark:text-gray-400">Sort:</label>
<select
[(ngModel)]="sortBy"
(change)="onSortChange()"
class="text-xs px-2 py-1 rounded border border-border dark:border-gray-600 bg-bg-primary dark:bg-gray-800 text-text-main dark:text-gray-100"
>
<option value="relevance">Relevance</option>
<option value="name">Name</option>
<option value="modified">Modified</option>
</select>
<!-- Expand/Collapse all -->
<button
(click)="toggleAllGroups()"
class="text-xs px-2 py-1 rounded hover:bg-bg-muted dark:hover:bg-gray-700 text-text-muted dark:text-gray-400"
title="Expand/Collapse all"
>
{{ allExpanded() ? 'Collapse all' : 'Expand all' }}
</button>
</div>
</div>
<!-- Results list -->
<div class="flex-1 overflow-y-auto">
@if (sortedGroups().length === 0) {
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-gray-400 p-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<p class="text-sm">No results found</p>
</div>
} @else {
<div class="divide-y divide-border dark:divide-gray-700">
@for (group of sortedGroups(); track group.noteId) {
<div class="hover:bg-bg-muted dark:hover:bg-gray-800 transition-colors">
<!-- File header -->
<div
class="flex items-center justify-between p-3 cursor-pointer"
(click)="toggleGroup(group)"
>
<div class="flex items-center gap-2 flex-1 min-w-0">
<!-- Expand/collapse icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 text-text-muted dark:text-gray-400 transition-transform"
[class.rotate-90]="group.isExpanded"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<!-- File icon -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-accent dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<!-- File name and path -->
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-text-main dark:text-gray-100 truncate">
{{ group.fileName }}
</div>
<div class="text-xs text-text-muted dark:text-gray-400 truncate">
{{ group.filePath }}
</div>
</div>
<!-- Match count badge -->
<span class="px-2 py-0.5 text-xs rounded-full bg-accent/10 dark:bg-blue-500/10 text-accent dark:text-blue-400">
{{ group.matchCount }} {{ group.matchCount === 1 ? 'match' : 'matches' }}
</span>
</div>
<!-- Open button -->
<button
(click)="openNote(group.noteId, $event)"
class="ml-2 p-1.5 rounded hover:bg-bg-primary dark:hover:bg-gray-700 text-text-muted dark:text-gray-400"
title="Open note"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</button>
</div>
<!-- Matches (expanded) -->
@if (group.isExpanded) {
<div class="px-3 pb-3 pl-11 space-y-2">
@for (match of group.matches; track $index) {
<div
class="p-2 rounded bg-bg-muted dark:bg-gray-800 hover:bg-bg-secondary dark:hover:bg-gray-700 cursor-pointer transition-colors"
(click)="openNote(group.noteId, $event, match.line)"
>
<!-- Match type badge -->
<div class="flex items-center gap-2 mb-1">
<span class="text-xs px-1.5 py-0.5 rounded bg-bg-primary dark:bg-gray-900 text-text-muted dark:text-gray-400 font-mono">
{{ match.type }}
</span>
@if (match.line) {
<span class="text-xs text-text-muted dark:text-gray-500">
Line {{ match.line }}
</span>
}
</div>
<!-- Match context with highlighting -->
<div class="text-sm text-text-main dark:text-gray-200 font-mono leading-relaxed">
<span [innerHTML]="highlightMatch(match.context, match.text)"></span>
</div>
</div>
}
</div>
}
</div>
}
</div>
}
</div>
</div>
`,
styles: [`
:host {
display: block;
height: 100%;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
:host-context(.dark) ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
`]
})
export class SearchResultsComponent {
@Input() set results(value: SearchResult[]) {
this.buildGroups(value);
}
@Output() noteOpen = new EventEmitter<{ noteId: string; line?: number }>();
private vaultService = inject(VaultService);
private groups = signal<ResultGroup[]>([]);
sortBy: SortOption = 'relevance';
/**
* Total number of result files
*/
totalResults = computed(() => this.groups().length);
/**
* Total number of matches across all files
*/
totalMatches = computed(() => {
return this.groups().reduce((sum, group) => sum + group.matchCount, 0);
});
/**
* Check if all groups are expanded
*/
allExpanded = computed(() => {
const groups = this.groups();
return groups.length > 0 && groups.every(g => g.isExpanded);
});
/**
* Sorted groups based on current sort option
*/
sortedGroups = computed(() => {
const groups = [...this.groups()];
switch (this.sortBy) {
case 'relevance':
return groups.sort((a, b) => b.score - a.score);
case 'name':
return groups.sort((a, b) => a.fileName.localeCompare(b.fileName));
case 'modified':
// Would need modification time from vault service
return groups;
default:
return groups;
}
});
/**
* Build result groups from search results
*/
private buildGroups(results: SearchResult[]): void {
const groups: ResultGroup[] = results.map(result => {
// Get note info from vault service
const note = this.vaultService.getNoteById(result.noteId);
const fileName = note?.fileName || result.noteId;
const filePath = note?.filePath || result.noteId;
return {
noteId: result.noteId,
fileName,
filePath,
matches: result.matches,
matchCount: result.matches.length,
isExpanded: true, // Expand by default
score: result.score
};
});
this.groups.set(groups);
}
/**
* Toggle a result group
*/
toggleGroup(group: ResultGroup): void {
this.groups.update(groups => {
return groups.map(g =>
g.noteId === group.noteId
? { ...g, isExpanded: !g.isExpanded }
: g
);
});
}
/**
* Toggle all groups
*/
toggleAllGroups(): void {
const shouldExpand = !this.allExpanded();
this.groups.update(groups => {
return groups.map(g => ({ ...g, isExpanded: shouldExpand }));
});
}
/**
* Handle sort change
*/
onSortChange(): void {
// Sorting is handled by computed property
}
/**
* Open a note
*/
openNote(noteId: string, event: Event, line?: number): void {
event.stopPropagation();
this.noteOpen.emit({ noteId, line });
}
/**
* Highlight matched text in context
*/
highlightMatch(context: string, matchText: string): string {
if (!matchText || !context) {
return this.escapeHtml(context);
}
const escapedContext = this.escapeHtml(context);
const escapedMatch = this.escapeHtml(matchText);
// Case-insensitive replacement with highlighting
const regex = new RegExp(`(${escapedMatch})`, 'gi');
return escapedContext.replace(
regex,
'<mark class="bg-yellow-200 dark:bg-yellow-600 text-text-main dark:text-gray-900 px-0.5 rounded">$1</mark>'
);
}
/**
* Escape HTML to prevent XSS
*/
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}