mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:21:08 +02:00
feat(manacore/web): add unified QuickInputBar with context-aware adapters
Implement adapter pattern for the QuickInputBar that automatically switches behavior based on the current route: - /todo: NL task parser (priority, dates, labels, subtasks) - /calendar: NL event parser (time, duration, recurrence, location) - /contacts: NL contact parser (company, email, phone, tags) - /times: search alarms and world clocks - /planta: NL plant parser (species, care actions) - fallback: cross-app search via SearchRegistry Architecture: InputBarAdapter interface, lazy-loaded adapters per module, route-reactive resolution in layout. Contact parser ported from standalone contacts app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
475ed87a41
commit
3bd717bc93
9 changed files with 576 additions and 0 deletions
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Calendar QuickInputBar Adapter
|
||||
*/
|
||||
|
||||
import type { InputBarAdapter } from '$lib/quick-input/types';
|
||||
import type { QuickInputItem } from '@manacore/shared-ui';
|
||||
import { db } from '$lib/data/database';
|
||||
import { parseEventInput, resolveEventIds, formatParsedEventPreview } from './utils/event-parser';
|
||||
import { toCalendar, toCalendarEvent } from './queries';
|
||||
import type { LocalCalendar, LocalEvent } from './types';
|
||||
import { format, isSameDay } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
|
||||
export function createAdapter(): InputBarAdapter {
|
||||
return {
|
||||
placeholder: 'Neuer Termin oder suchen...',
|
||||
appIcon: 'calendar',
|
||||
deferSearch: true,
|
||||
createText: 'Erstellen',
|
||||
emptyText: 'Keine Termine gefunden',
|
||||
|
||||
async onSearch(query) {
|
||||
const q = query.toLowerCase();
|
||||
const events = await db.table<LocalEvent>('events').toArray();
|
||||
return events
|
||||
.filter((e) => !e.deletedAt && e.title?.toLowerCase().includes(q))
|
||||
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
|
||||
.slice(0, 10)
|
||||
.map((e) => ({
|
||||
id: e.id,
|
||||
title: e.title || '',
|
||||
subtitle: e.startDate
|
||||
? format(new Date(e.startDate), 'dd. MMM yyyy, HH:mm', { locale: de })
|
||||
: '',
|
||||
}));
|
||||
},
|
||||
|
||||
onSelect(item: QuickInputItem) {
|
||||
// Could open event detail modal — for now just navigate
|
||||
window.dispatchEvent(new CustomEvent('calendar:open-event', { detail: { id: item.id } }));
|
||||
},
|
||||
|
||||
onParseCreate(query) {
|
||||
if (!query.trim()) return null;
|
||||
const parsed = parseEventInput(query);
|
||||
const preview = formatParsedEventPreview(parsed);
|
||||
return {
|
||||
title: `"${parsed.title}" erstellen`,
|
||||
subtitle: preview || 'Neuer Termin',
|
||||
};
|
||||
},
|
||||
|
||||
async onCreate(query) {
|
||||
if (!query.trim()) return;
|
||||
const parsed = parseEventInput(query);
|
||||
const calendars = (await db.table<LocalCalendar>('calendars').toArray())
|
||||
.filter((c) => !c.deletedAt)
|
||||
.map(toCalendar);
|
||||
const tags = await db.table('tags').toArray();
|
||||
const defaultCal = calendars.find((c) => c.isDefault) || calendars[0];
|
||||
const resolved = resolveEventIds(parsed, calendars, tags, defaultCal?.id);
|
||||
|
||||
const { eventsStore } = await import('./stores/events.svelte');
|
||||
await eventsStore.createEvent({
|
||||
calendarId: resolved.calendarId || defaultCal?.id || '',
|
||||
title: resolved.title,
|
||||
description: null,
|
||||
startTime: resolved.startTime || new Date().toISOString(),
|
||||
endTime: resolved.endTime || new Date(Date.now() + 3600000).toISOString(),
|
||||
isAllDay: resolved.isAllDay || false,
|
||||
location: resolved.location || null,
|
||||
recurrenceRule: resolved.recurrenceRule || null,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Contacts QuickInputBar Adapter
|
||||
*/
|
||||
|
||||
import type { InputBarAdapter } from '$lib/quick-input/types';
|
||||
import type { QuickInputItem } from '@manacore/shared-ui';
|
||||
import { db } from '$lib/data/database';
|
||||
import { parseContactInput, formatParsedContactPreview } from './utils/contact-parser';
|
||||
import type { LocalContact } from './types';
|
||||
import { contactModalStore } from './stores/modal.svelte';
|
||||
|
||||
export function createAdapter(): InputBarAdapter {
|
||||
return {
|
||||
placeholder: 'Neuer Kontakt oder suchen...',
|
||||
appIcon: 'contacts',
|
||||
deferSearch: true,
|
||||
createText: 'Erstellen',
|
||||
emptyText: 'Keine Kontakte gefunden',
|
||||
|
||||
async onSearch(query) {
|
||||
const q = query.toLowerCase();
|
||||
const contacts = await db.table<LocalContact>('contacts').toArray();
|
||||
return contacts
|
||||
.filter(
|
||||
(c) =>
|
||||
!c.deletedAt &&
|
||||
!c.isArchived &&
|
||||
(c.firstName?.toLowerCase().includes(q) ||
|
||||
c.lastName?.toLowerCase().includes(q) ||
|
||||
c.email?.toLowerCase().includes(q) ||
|
||||
c.company?.toLowerCase().includes(q))
|
||||
)
|
||||
.slice(0, 10)
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
title: [c.firstName, c.lastName].filter(Boolean).join(' ') || c.email || '',
|
||||
subtitle: c.company || c.email || '',
|
||||
}));
|
||||
},
|
||||
|
||||
onSelect(item: QuickInputItem) {
|
||||
// Navigate to contact or open detail
|
||||
window.location.hash = `contact=${item.id}`;
|
||||
},
|
||||
|
||||
onParseCreate(query) {
|
||||
if (!query.trim()) return null;
|
||||
const parsed = parseContactInput(query);
|
||||
const preview = formatParsedContactPreview(parsed);
|
||||
return {
|
||||
title: parsed.displayName ? `"${parsed.displayName}" erstellen` : 'Kontakt erstellen',
|
||||
subtitle: preview || 'Neuer Kontakt',
|
||||
};
|
||||
},
|
||||
|
||||
async onCreate(query) {
|
||||
if (!query.trim()) return;
|
||||
const parsed = parseContactInput(query);
|
||||
// Open the contact modal with prefilled data
|
||||
contactModalStore.open({
|
||||
firstName: parsed.firstName,
|
||||
lastName: parsed.lastName,
|
||||
email: parsed.email,
|
||||
phone: parsed.phone,
|
||||
company: parsed.company,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* Contact Parser — Natural language contact input parsing.
|
||||
*
|
||||
* Ported from apps/contacts/apps/web/src/lib/utils/contact-parser.ts
|
||||
*
|
||||
* Examples:
|
||||
* - "Max Mustermann @ACME Corp max@example.com #kunde"
|
||||
* - "Anna Schmidt bei Google +49 123 456789"
|
||||
*/
|
||||
|
||||
import { extractTags, extractAtReference } from '@manacore/shared-utils';
|
||||
|
||||
export interface ParsedContact {
|
||||
displayName: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
company?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
tagNames: string[];
|
||||
}
|
||||
|
||||
export interface ParsedContactWithIds {
|
||||
displayName: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
company?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
tagIds: string[];
|
||||
}
|
||||
|
||||
const EMAIL_PATTERN = /\b([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\b/;
|
||||
|
||||
const PHONE_PATTERNS: RegExp[] = [
|
||||
/\+\d{1,3}[-\s]?\d{2,4}[-\s]?\d{3,}[-\s]?\d*/,
|
||||
/\b0\d{2,4}[-\s/]?\d{3,}[-\s]?\d*/,
|
||||
/\b\d{6,}\b/,
|
||||
];
|
||||
|
||||
const COMPANY_PATTERNS: RegExp[] = [
|
||||
/\bbei\s+([^@#]+?)(?=\s+(?:@|#|\+|[a-zA-Z0-9._%+-]+@)|$)/i,
|
||||
/\bvon\s+([^@#]+?)(?=\s+(?:@|#|\+|[a-zA-Z0-9._%+-]+@)|$)/i,
|
||||
];
|
||||
|
||||
function extractEmail(text: string): { email?: string; remaining: string } {
|
||||
const match = text.match(EMAIL_PATTERN);
|
||||
if (match) {
|
||||
return { email: match[1], remaining: text.replace(EMAIL_PATTERN, '').trim() };
|
||||
}
|
||||
return { email: undefined, remaining: text };
|
||||
}
|
||||
|
||||
function extractPhone(text: string): { phone?: string; remaining: string } {
|
||||
for (const pattern of PHONE_PATTERNS) {
|
||||
const match = text.match(pattern);
|
||||
if (match) {
|
||||
return { phone: match[0].trim(), remaining: text.replace(pattern, '').trim() };
|
||||
}
|
||||
}
|
||||
return { phone: undefined, remaining: text };
|
||||
}
|
||||
|
||||
function extractCompanyPattern(text: string): { company?: string; remaining: string } {
|
||||
for (const pattern of COMPANY_PATTERNS) {
|
||||
const match = text.match(pattern);
|
||||
if (match) {
|
||||
return { company: match[1].trim(), remaining: text.replace(pattern, '').trim() };
|
||||
}
|
||||
}
|
||||
return { company: undefined, remaining: text };
|
||||
}
|
||||
|
||||
function parseNames(displayName: string): { firstName?: string; lastName?: string } {
|
||||
const parts = displayName.trim().split(/\s+/);
|
||||
if (parts.length === 0) return {};
|
||||
if (parts.length === 1) return { firstName: parts[0] };
|
||||
return { firstName: parts[0], lastName: parts.slice(1).join(' ') };
|
||||
}
|
||||
|
||||
export function parseContactInput(input: string): ParsedContact {
|
||||
let text = input.trim();
|
||||
|
||||
const tagsResult = extractTags(text);
|
||||
text = tagsResult.remaining;
|
||||
const tagNames = tagsResult.value || [];
|
||||
|
||||
const atRefResult = extractAtReference(text);
|
||||
text = atRefResult.remaining;
|
||||
let company = atRefResult.value;
|
||||
|
||||
if (!company) {
|
||||
const companyPatternResult = extractCompanyPattern(text);
|
||||
text = companyPatternResult.remaining;
|
||||
company = companyPatternResult.company;
|
||||
}
|
||||
|
||||
const emailResult = extractEmail(text);
|
||||
text = emailResult.remaining;
|
||||
|
||||
const phoneResult = extractPhone(text);
|
||||
text = phoneResult.remaining;
|
||||
|
||||
const displayName = text.replace(/\s+/g, ' ').trim();
|
||||
const { firstName, lastName } = parseNames(displayName);
|
||||
|
||||
return {
|
||||
displayName,
|
||||
firstName,
|
||||
lastName,
|
||||
company,
|
||||
email: emailResult.email,
|
||||
phone: phoneResult.phone,
|
||||
tagNames,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveContactIds(
|
||||
parsed: ParsedContact,
|
||||
tags: { id: string; name: string }[]
|
||||
): ParsedContactWithIds {
|
||||
const tagIds = parsed.tagNames
|
||||
.map((name) => tags.find((t) => t.name.toLowerCase() === name.toLowerCase())?.id)
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
return {
|
||||
displayName: parsed.displayName,
|
||||
firstName: parsed.firstName,
|
||||
lastName: parsed.lastName,
|
||||
company: parsed.company,
|
||||
email: parsed.email,
|
||||
phone: parsed.phone,
|
||||
tagIds,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatParsedContactPreview(parsed: ParsedContact): string {
|
||||
const parts: string[] = [];
|
||||
if (parsed.company) parts.push(parsed.company);
|
||||
if (parsed.email) parts.push(parsed.email);
|
||||
if (parsed.phone) parts.push(parsed.phone);
|
||||
if (parsed.tagNames.length > 0) parts.push(parsed.tagNames.map((t) => `#${t}`).join(' '));
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Planta QuickInputBar Adapter
|
||||
*/
|
||||
|
||||
import type { InputBarAdapter } from '$lib/quick-input/types';
|
||||
import type { QuickInputItem } from '@manacore/shared-ui';
|
||||
import { db } from '$lib/data/database';
|
||||
import { parsePlantInput, formatParsedPlantPreview } from './utils/plant-parser';
|
||||
import { plantTable } from './collections';
|
||||
|
||||
export function createAdapter(): InputBarAdapter {
|
||||
return {
|
||||
placeholder: 'Neue Pflanze oder suchen...',
|
||||
appIcon: 'plant',
|
||||
deferSearch: true,
|
||||
createText: 'Hinzufügen',
|
||||
emptyText: 'Keine Pflanzen gefunden',
|
||||
|
||||
async onSearch(query) {
|
||||
const q = query.toLowerCase();
|
||||
const plants = await db.table('plants').toArray();
|
||||
return (plants as Record<string, unknown>[])
|
||||
.filter(
|
||||
(p) =>
|
||||
!(p.deletedAt as string) &&
|
||||
((p.name as string)?.toLowerCase().includes(q) ||
|
||||
(p.species as string)?.toLowerCase().includes(q))
|
||||
)
|
||||
.slice(0, 10)
|
||||
.map((p) => ({
|
||||
id: p.id as string,
|
||||
title: (p.name as string) || '',
|
||||
subtitle: (p.species as string) || (p.location as string) || '',
|
||||
}));
|
||||
},
|
||||
|
||||
onSelect() {},
|
||||
|
||||
onParseCreate(query) {
|
||||
if (!query.trim()) return null;
|
||||
const parsed = parsePlantInput(query);
|
||||
const preview = formatParsedPlantPreview(parsed);
|
||||
return {
|
||||
title: `"${parsed.name}" hinzufügen`,
|
||||
subtitle: preview || 'Neue Pflanze',
|
||||
};
|
||||
},
|
||||
|
||||
async onCreate(query) {
|
||||
if (!query.trim()) return;
|
||||
const parsed = parsePlantInput(query);
|
||||
await plantTable.add({
|
||||
id: crypto.randomUUID(),
|
||||
name: parsed.name,
|
||||
species: parsed.species,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Times QuickInputBar Adapter
|
||||
*
|
||||
* Provides search across time entries (stopwatch sessions)
|
||||
* and quick-create for new timer/alarm entries.
|
||||
*/
|
||||
|
||||
import type { InputBarAdapter } from '$lib/quick-input/types';
|
||||
import type { QuickInputItem } from '@manacore/shared-ui';
|
||||
import { db } from '$lib/data/database';
|
||||
import { parseEntryInput, formatParsedEntryPreview } from './utils/entry-parser';
|
||||
|
||||
export function createAdapter(): InputBarAdapter {
|
||||
return {
|
||||
placeholder: 'Suchen in Times...',
|
||||
appIcon: 'clock',
|
||||
deferSearch: false,
|
||||
emptyText: 'Keine Einträge gefunden',
|
||||
|
||||
async onSearch(query) {
|
||||
const q = query.toLowerCase();
|
||||
// Search across alarms and world clocks
|
||||
const alarms = await db.table('alarms').toArray();
|
||||
const worldClocks = await db.table('worldClocks').toArray();
|
||||
|
||||
const results: QuickInputItem[] = [];
|
||||
|
||||
for (const a of alarms as Record<string, unknown>[]) {
|
||||
if (!(a.deletedAt as string) && (a.label as string)?.toLowerCase().includes(q)) {
|
||||
results.push({
|
||||
id: a.id as string,
|
||||
title: (a.label as string) || 'Alarm',
|
||||
subtitle: `${(a.hour as number)?.toString().padStart(2, '0')}:${(a.minute as number)?.toString().padStart(2, '0')}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const wc of worldClocks as Record<string, unknown>[]) {
|
||||
if (
|
||||
!(wc.deletedAt as string) &&
|
||||
((wc.label as string)?.toLowerCase().includes(q) ||
|
||||
(wc.timezone as string)?.toLowerCase().includes(q))
|
||||
) {
|
||||
results.push({
|
||||
id: wc.id as string,
|
||||
title: (wc.label as string) || (wc.timezone as string) || '',
|
||||
subtitle: 'Weltuhr',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results.slice(0, 10);
|
||||
},
|
||||
|
||||
onSelect() {},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* Todo QuickInputBar Adapter
|
||||
*/
|
||||
|
||||
import type { InputBarAdapter } from '$lib/quick-input/types';
|
||||
import type { QuickInputItem } from '@manacore/shared-ui';
|
||||
import { goto } from '$app/navigation';
|
||||
import { db } from '$lib/data/database';
|
||||
import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from './utils/task-parser';
|
||||
import type { LocalTask } from './types';
|
||||
|
||||
export function createAdapter(): InputBarAdapter {
|
||||
return {
|
||||
placeholder: 'Neue Aufgabe oder suchen...',
|
||||
appIcon: 'todo',
|
||||
deferSearch: true,
|
||||
createText: 'Erstellen',
|
||||
emptyText: 'Keine Aufgaben gefunden',
|
||||
|
||||
async onSearch(query) {
|
||||
const q = query.toLowerCase();
|
||||
const tasks = await db.table<LocalTask>('tasks').toArray();
|
||||
return tasks
|
||||
.filter(
|
||||
(t) =>
|
||||
!t.deletedAt &&
|
||||
!t.isCompleted &&
|
||||
(t.title?.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q))
|
||||
)
|
||||
.slice(0, 10)
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title || '',
|
||||
subtitle: t.dueDate ? new Date(t.dueDate).toLocaleDateString('de-DE') : 'Keine Frist',
|
||||
}));
|
||||
},
|
||||
|
||||
onSelect(item: QuickInputItem) {
|
||||
goto(`/todo?task=${item.id}`);
|
||||
},
|
||||
|
||||
onParseCreate(query) {
|
||||
if (!query.trim()) return null;
|
||||
const parsed = parseTaskInput(query);
|
||||
return {
|
||||
title: `"${parsed.title}" erstellen`,
|
||||
subtitle: formatParsedTaskPreview(parsed) || 'Neue Aufgabe',
|
||||
};
|
||||
},
|
||||
|
||||
async onCreate(query) {
|
||||
if (!query.trim()) return;
|
||||
const parsed = parseTaskInput(query);
|
||||
const allTags = await db.table('tags').toArray();
|
||||
const resolved = resolveTaskIds(parsed, allTags);
|
||||
const { tasksStore } = await import('./stores/tasks.svelte');
|
||||
await tasksStore.createTask({
|
||||
title: resolved.title,
|
||||
dueDate: resolved.dueDate,
|
||||
priority: resolved.priority,
|
||||
labelIds: resolved.labelIds,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Fallback Adapter — Global cross-app search for non-module pages.
|
||||
*
|
||||
* Used on /home, /dashboard, /settings, etc. where no specific module
|
||||
* is active. Delegates to the SearchRegistry to search across all apps.
|
||||
*/
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import type { QuickInputItem } from '@manacore/shared-ui';
|
||||
import type { SearchRegistry } from '$lib/search/registry';
|
||||
import type { InputBarAdapter } from './types';
|
||||
|
||||
export function createFallbackAdapter(searchRegistry: SearchRegistry): InputBarAdapter {
|
||||
return {
|
||||
placeholder: 'Suchen...',
|
||||
appIcon: 'search',
|
||||
deferSearch: false,
|
||||
emptyText: 'Keine Ergebnisse gefunden',
|
||||
|
||||
async onSearch(query: string): Promise<QuickInputItem[]> {
|
||||
const groups = await searchRegistry.search(query, { limit: 5 });
|
||||
return groups
|
||||
.flatMap((g) =>
|
||||
g.results.map((r) => ({
|
||||
id: `${r.appId}:${r.id}`,
|
||||
title: r.title,
|
||||
subtitle: `${g.appName} · ${r.subtitle ?? r.type}`,
|
||||
icon: g.appIcon,
|
||||
_href: r.href,
|
||||
}))
|
||||
)
|
||||
.slice(0, 10) as QuickInputItem[];
|
||||
},
|
||||
|
||||
onSelect(item: QuickInputItem) {
|
||||
const href = (item as QuickInputItem & { _href?: string })._href;
|
||||
if (href) goto(href);
|
||||
},
|
||||
};
|
||||
}
|
||||
29
apps/manacore/apps/web/src/lib/quick-input/registry.ts
Normal file
29
apps/manacore/apps/web/src/lib/quick-input/registry.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Adapter Registry — Maps route prefixes to lazy adapter loaders.
|
||||
*
|
||||
* Each entry loads the module's adapter only when the user navigates
|
||||
* to that module, keeping the initial bundle small.
|
||||
*/
|
||||
|
||||
type AdapterModule = { createAdapter: (...args: unknown[]) => unknown };
|
||||
|
||||
const registry = new Map<string, () => Promise<AdapterModule>>([
|
||||
['/todo', () => import('$lib/modules/todo/quick-input-adapter')],
|
||||
['/calendar', () => import('$lib/modules/calendar/quick-input-adapter')],
|
||||
['/contacts', () => import('$lib/modules/contacts/quick-input-adapter')],
|
||||
['/times', () => import('$lib/modules/times/quick-input-adapter')],
|
||||
['/planta', () => import('$lib/modules/planta/quick-input-adapter')],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Find the adapter loader for a given pathname.
|
||||
* Returns null if no module matches (fallback adapter should be used).
|
||||
*/
|
||||
export function getAdapterLoader(pathname: string): (() => Promise<AdapterModule>) | null {
|
||||
for (const [prefix, loader] of registry) {
|
||||
if (pathname === prefix || pathname.startsWith(prefix + '/')) {
|
||||
return loader;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
37
apps/manacore/apps/web/src/lib/quick-input/types.ts
Normal file
37
apps/manacore/apps/web/src/lib/quick-input/types.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Unified QuickInputBar Adapter — Type Definitions
|
||||
*
|
||||
* Each module implements InputBarAdapter to provide context-aware
|
||||
* search, create-preview, and create behavior for the QuickInputBar.
|
||||
*/
|
||||
|
||||
import type { QuickInputItem, CreatePreview, HighlightPattern } from '@manacore/shared-ui';
|
||||
|
||||
export interface InputBarAdapter {
|
||||
// Required
|
||||
onSearch: (query: string) => Promise<QuickInputItem[]>;
|
||||
onSelect: (item: QuickInputItem) => void;
|
||||
|
||||
// Create (optional — modules without create don't set these)
|
||||
onParseCreate?: (query: string) => CreatePreview | null;
|
||||
onCreate?: (query: string) => Promise<void>;
|
||||
onSearchChange?: (query: string, results: QuickInputItem[]) => void;
|
||||
|
||||
// Display
|
||||
placeholder: string;
|
||||
appIcon: string;
|
||||
emptyText?: string;
|
||||
createText?: string;
|
||||
deferSearch?: boolean;
|
||||
|
||||
// Calendar-style default selector
|
||||
defaultOptions?: { id: string; label: string }[];
|
||||
selectedDefaultId?: string;
|
||||
defaultOptionLabel?: string;
|
||||
onDefaultChange?: (id: string) => void;
|
||||
|
||||
// Highlight patterns
|
||||
highlightPatterns?: HighlightPattern[];
|
||||
}
|
||||
|
||||
export type InputBarAdapterFactory = () => InputBarAdapter;
|
||||
Loading…
Add table
Add a link
Reference in a new issue