mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
feat(shared-ui): locale-aware highlighting + success feedback for InputBar
1. Extract hardcoded German highlight patterns into locale-specific sets (de, en, fr, it, es). InputBar accepts `locale` or custom `highlightPatterns` prop, defaulting to German for backward compat. 2. Add visual success feedback after creating: input bar flashes green with a checkmark icon for 1.2s, confirming the action was successful. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
45db42720c
commit
9edd1c6e2e
5 changed files with 227 additions and 62 deletions
|
|
@ -398,6 +398,7 @@
|
|||
onParseCreate={handleParseCreate}
|
||||
createText="Erstellen"
|
||||
deferSearch={true}
|
||||
locale={$locale || 'de'}
|
||||
appIcon="todo"
|
||||
hasFabRight={true}
|
||||
bottomOffset={isPillNavCollapsed ? '16px' : isFilterStripVisible ? '180px' : '110px'}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { QuickInputItem, CreatePreview } from './types';
|
||||
import type { QuickInputItem, CreatePreview, HighlightPattern } from './types';
|
||||
import InputBarContextMenu from './InputBarContextMenu.svelte';
|
||||
import { getInputBarSettingsStore } from './inputBarSettings.svelte';
|
||||
import { getHighlightPatterns } from './highlightPatterns';
|
||||
|
||||
// Settings store
|
||||
const settingsStore = getInputBarSettingsStore();
|
||||
|
||||
// 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 {
|
||||
function highlightText(text: string, patterns: HighlightPattern[]): string {
|
||||
if (!text) return '';
|
||||
|
||||
let result = text;
|
||||
|
|
@ -42,7 +17,7 @@
|
|||
result = result.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Apply highlights (process in order, avoiding double-highlighting)
|
||||
for (const { pattern, className } of HIGHLIGHT_PATTERNS) {
|
||||
for (const { pattern, className } of patterns) {
|
||||
result = result.replace(pattern, (match) => `<span class="${className}">${match}</span>`);
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +67,10 @@
|
|||
onShowSyntaxHelp?: () => void;
|
||||
/** Snippet for left action button (e.g., voice input) - rendered inside the input bar on the left */
|
||||
leftAction?: Snippet;
|
||||
/** Custom highlight patterns. If not provided, uses locale-based defaults. */
|
||||
highlightPatterns?: HighlightPattern[];
|
||||
/** Locale for syntax highlighting keywords (e.g., 'de', 'en'). Default: 'de'. */
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -118,6 +97,8 @@
|
|||
onShowShortcuts,
|
||||
onShowSyntaxHelp,
|
||||
leftAction,
|
||||
highlightPatterns,
|
||||
locale = 'de',
|
||||
}: Props = $props();
|
||||
|
||||
// Use settings for autoFocus
|
||||
|
|
@ -127,10 +108,12 @@
|
|||
let results = $state<QuickInputItem[]>([]);
|
||||
let loading = $state(false);
|
||||
let creating = $state(false);
|
||||
let createSuccess = $state(false);
|
||||
let selectedIndex = $state(0);
|
||||
let showPanel = $state(false);
|
||||
let isFocused = $state(false);
|
||||
let searchTimeout: ReturnType<typeof setTimeout>;
|
||||
let createSuccessTimeout: ReturnType<typeof setTimeout>;
|
||||
let inputElement = $state<HTMLInputElement | null>(null);
|
||||
// Whether search has been explicitly triggered in deferred mode
|
||||
let searchTriggered = $state(false);
|
||||
|
|
@ -145,9 +128,12 @@
|
|||
searchQuery.trim() && onParseCreate ? onParseCreate(searchQuery) : null
|
||||
);
|
||||
|
||||
// Resolve highlight patterns: custom prop > locale-based defaults
|
||||
let effectivePatterns = $derived(highlightPatterns ?? getHighlightPatterns(locale));
|
||||
|
||||
// Highlighted text for overlay (respects syntax highlighting setting)
|
||||
let highlightedQuery = $derived(
|
||||
settingsStore.syntaxHighlighting ? highlightText(searchQuery) : searchQuery
|
||||
settingsStore.syntaxHighlighting ? highlightText(searchQuery, effectivePatterns) : searchQuery
|
||||
);
|
||||
|
||||
// Check if create option is selected (it's always first when available)
|
||||
|
|
@ -253,11 +239,19 @@
|
|||
selectedIndex = 0;
|
||||
searchTriggered = false;
|
||||
onSearchChange?.('', []);
|
||||
|
||||
// Show success feedback
|
||||
creating = false;
|
||||
createSuccess = true;
|
||||
clearTimeout(createSuccessTimeout);
|
||||
createSuccessTimeout = setTimeout(() => {
|
||||
createSuccess = false;
|
||||
}, 1200);
|
||||
|
||||
// Keep focus for rapid entry
|
||||
inputElement?.focus();
|
||||
} catch (error) {
|
||||
console.error('Create error:', error);
|
||||
} finally {
|
||||
creating = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -495,7 +489,11 @@
|
|||
|
||||
<!-- Input Bar (always visible) -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="input-container" oncontextmenu={handleContextMenu}>
|
||||
<div
|
||||
class="input-container"
|
||||
class:create-success={createSuccess}
|
||||
oncontextmenu={handleContextMenu}
|
||||
>
|
||||
<!-- Left action slot (e.g., voice input button) -->
|
||||
{#if leftAction}
|
||||
<div class="left-action">
|
||||
|
|
@ -503,39 +501,51 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="app-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{#if appIcon === 'check-square' || appIcon === 'todo'}
|
||||
<div class="app-icon" class:success-icon={createSuccess}>
|
||||
{#if createSuccess}
|
||||
<!-- Checkmark icon -->
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||
stroke-width="2.5"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
{:else if appIcon === 'calendar'}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
{:else if appIcon === 'users' || appIcon === 'contacts'}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
{:else}
|
||||
<!-- Default search icon -->
|
||||
<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"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{#if appIcon === 'check-square' || appIcon === 'todo'}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||
/>
|
||||
{:else if appIcon === 'calendar'}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
{:else if appIcon === 'users' || appIcon === 'contacts'}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
{:else}
|
||||
<!-- Default search icon -->
|
||||
<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"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="input-wrapper">
|
||||
|
|
@ -656,6 +666,48 @@
|
|||
0 0 0 2px hsl(var(--color-primary) / 0.25);
|
||||
}
|
||||
|
||||
/* Success flash after creating */
|
||||
.input-container.create-success {
|
||||
border-color: hsl(var(--color-success, 142 71% 45%));
|
||||
box-shadow:
|
||||
0 4px 6px -1px hsl(var(--color-foreground) / 0.1),
|
||||
0 2px 4px -1px hsl(var(--color-foreground) / 0.06),
|
||||
0 0 0 2px hsl(var(--color-success, 142 71% 45%) / 0.3);
|
||||
animation: success-flash 1.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes success-flash {
|
||||
0% {
|
||||
border-color: hsl(var(--color-success, 142 71% 45%));
|
||||
background: hsl(var(--color-success, 142 71% 45%) / 0.15);
|
||||
}
|
||||
40% {
|
||||
background: hsl(var(--color-success, 142 71% 45%) / 0.08);
|
||||
}
|
||||
100% {
|
||||
background: hsl(var(--color-surface) / 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.app-icon.success-icon {
|
||||
color: hsl(var(--color-success, 142 71% 45%));
|
||||
animation: success-check 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes success-check {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.left-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
104
packages/shared-ui/src/quick-input/highlightPatterns.ts
Normal file
104
packages/shared-ui/src/quick-input/highlightPatterns.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import type { HighlightPattern } from './types';
|
||||
|
||||
/** Shared patterns that work across all locales (symbols, not words) */
|
||||
const UNIVERSAL_PATTERNS: HighlightPattern[] = [
|
||||
// Priority shortcuts: !!! = urgent, !! = high
|
||||
{ pattern: /!{3,}/g, className: 'hl-priority-urgent' },
|
||||
{ pattern: /!{2}/g, className: 'hl-priority-high' },
|
||||
// Tags
|
||||
{ pattern: /#\w+/g, className: 'hl-tag' },
|
||||
// Projects/Calendars/Companies (@reference)
|
||||
{ pattern: /@\w+/g, className: 'hl-reference' },
|
||||
// Time patterns (universal formats)
|
||||
{ pattern: /\b\d{1,2}:\d{2}\b/g, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** German date/priority keywords */
|
||||
const DE_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?dringend\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?wichtig\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?normal\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?sp[aä]ter\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
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',
|
||||
},
|
||||
{ pattern: /\b(um\s+\d{1,2}(\s*uhr)?|\d{1,2}\s*uhr)\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** English date/priority keywords */
|
||||
const EN_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgent\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(important|high)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normal|medium)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(low|later)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(today|tomorrow|yesterday|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next\s+\w+|in\s+\d+\s+days?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(at\s+\d{1,2}(:\d{2})?\s*(am|pm)?)\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** French date/priority keywords */
|
||||
const FR_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgent[e]?\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(important[e]?|haut[e]?)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normal[e]?|moyen(?:ne)?)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(bas(?:se)?|plus\s+tard)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(aujourd'hui|demain|après-demain|hier|lundi|mardi|mercredi|jeudi|vendredi|samedi|dimanche|prochain[e]?\s+\w+|dans\s+\d+\s+jours?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(à\s+\d{1,2}[h:]\d{0,2})\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** Italian date/priority keywords */
|
||||
const IT_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgente\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(importante|alto)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normale|medio)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(basso|dopo)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(oggi|domani|dopodomani|ieri|luned[ìi]|marted[ìi]|mercoled[ìi]|gioved[ìi]|venerd[ìi]|sabato|domenica|prossim[oa]\s+\w+|tra\s+\d+\s+giorn[oi])\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(alle\s+\d{1,2}[.:]\d{0,2})\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** Spanish date/priority keywords */
|
||||
const ES_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgente\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(importante|alto)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normal|medio)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(bajo|luego)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(hoy|mañana|pasado\s+mañana|ayer|lunes|martes|miércoles|jueves|viernes|sábado|domingo|próxim[oa]\s+\w+|en\s+\d+\s+d[ií]as?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(a\s+las?\s+\d{1,2}[.:]\d{0,2})\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
const LOCALE_PATTERNS: Record<string, HighlightPattern[]> = {
|
||||
de: DE_PATTERNS,
|
||||
en: EN_PATTERNS,
|
||||
fr: FR_PATTERNS,
|
||||
it: IT_PATTERNS,
|
||||
es: ES_PATTERNS,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get highlight patterns for a given locale.
|
||||
* Returns universal patterns + locale-specific patterns.
|
||||
* Falls back to German if locale is unknown.
|
||||
*/
|
||||
export function getHighlightPatterns(locale = 'de'): HighlightPattern[] {
|
||||
// Normalize locale (e.g., "en-US" -> "en")
|
||||
const lang = locale.split('-')[0].toLowerCase();
|
||||
const localeSpecific = LOCALE_PATTERNS[lang] || LOCALE_PATTERNS['de'];
|
||||
return [...UNIVERSAL_PATTERNS, ...localeSpecific];
|
||||
}
|
||||
|
|
@ -3,7 +3,10 @@ export { default as InputBar } from './InputBar.svelte';
|
|||
export { default as QuickInputBar } from './InputBar.svelte';
|
||||
export { default as InputBarContextMenu } from './InputBarContextMenu.svelte';
|
||||
export { default as InputBarHelpModal } from './InputBarHelpModal.svelte';
|
||||
export type { QuickInputItem, QuickAction, CreatePreview } from './types';
|
||||
export type { QuickInputItem, QuickAction, CreatePreview, HighlightPattern } from './types';
|
||||
|
||||
// Highlight patterns (locale-aware syntax highlighting)
|
||||
export { getHighlightPatterns } from './highlightPatterns';
|
||||
|
||||
// Recent input history (tags, references)
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -20,3 +20,8 @@ export interface CreatePreview {
|
|||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
export interface HighlightPattern {
|
||||
pattern: RegExp;
|
||||
className: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue