refactor(shared-ui): extract search-core for highlight + debounce

Both QuickInputBar (InputBar.svelte), CommandBar.svelte, and GlobalSpotlight
were duplicating syntax highlighting and the 150ms search debounce. Pull
these into a new `packages/shared-ui/src/search-core/` module so the two
input surfaces stay in sync on feel and matching rules.

- search-core/highlight.ts — HighlightPattern type, locale-aware
  getHighlightPatterns(), and the shared highlightText() (HTML-escape +
  span wrap). Patterns were previously in quick-input/highlightPatterns.ts
  + inline in CommandBar.svelte.
- search-core/config.ts — SEARCH_DEBOUNCE_MS = 150. Used from InputBar,
  CommandBar, GlobalSpotlight, and apps/mana web SearchEngine.
- quick-input/highlightPatterns.ts + types.ts become thin back-compat
  re-exports.
- Public surface: @mana/shared-ui now exports getHighlightPatterns,
  highlightText, SEARCH_DEBOUNCE_MS, and the HighlightPattern type.

No UX change. UIs still live in their own files (per earlier split
recommendation: shared backend, separate surfaces).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 01:06:37 +02:00
parent ad1659f036
commit 24eb8b3b7f
10 changed files with 161 additions and 204 deletions

View file

@ -2,47 +2,9 @@
import { goto } from '$app/navigation';
import type { CommandBarItem, QuickAction, CreatePreview } from './CommandBar.types';
import { Heart, MagnifyingGlass, Plus } from '@mana/shared-icons';
import { getHighlightPatterns, highlightText, SEARCH_DEBOUNCE_MS } from '../search-core';
// Syntax highlighting patterns for command keywords
interface HighlightPattern {
pattern: RegExp;
className: string;
}
const HIGHLIGHT_PATTERNS: HighlightPattern[] = [
// Priority keywords (Todo) - with specific colors per level
{ pattern: /(!{3,}|!?dringend)\b/gi, className: 'hl-priority-urgent' },
{ pattern: /(!{2}|!?wichtig)\b/gi, className: 'hl-priority-high' },
{ pattern: /!?normal\b/gi, className: 'hl-priority-medium' },
{ pattern: /!?sp[aä]ter\b/gi, className: 'hl-priority-low' },
// Tags
{ pattern: /#\w+/g, className: 'hl-tag' },
// Projects/Calendars/Companies (@reference)
{ pattern: /@\w+/g, className: 'hl-reference' },
// Date keywords
{
pattern:
/\b(heute|morgen|übermorgen|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag|nächsten?\s+\w+|in\s+\d+\s+tagen?)\b/gi,
className: 'hl-date',
},
// Time patterns
{ pattern: /\b(\d{1,2}:\d{2}|um\s+\d{1,2}(\s*uhr)?|\d{1,2}\s*uhr)\b/gi, className: 'hl-time' },
];
function highlightText(text: string): string {
if (!text) return '';
let result = text;
// Escape HTML first
result = result.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// Apply highlights (process in order, avoiding double-highlighting)
for (const { pattern, className } of HIGHLIGHT_PATTERNS) {
result = result.replace(pattern, (match) => `<span class="${className}">${match}</span>`);
}
return result;
}
const HIGHLIGHT_PATTERNS = getHighlightPatterns('de');
interface Props {
open: boolean;
@ -89,7 +51,7 @@
);
// Highlighted text for overlay
let highlightedQuery = $derived(highlightText(searchQuery));
let highlightedQuery = $derived(highlightText(searchQuery, HIGHLIGHT_PATTERNS));
// Check if create option is selected (it's always first when available)
let isCreateSelected = $derived(selectedIndex === 0 && createPreview !== null);
@ -126,7 +88,7 @@
} finally {
loading = false;
}
}, 150);
}, SEARCH_DEBOUNCE_MS);
}
async function handleCreate() {