diff --git a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte index 52066a92b..46b63932c 100644 --- a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte @@ -3,7 +3,7 @@ import { page } from '$app/stores'; import { onMount } from 'svelte'; import { locale } from 'svelte-i18n'; - import { PillNavigation, QuickInputBar } from '@manacore/shared-ui'; + import { PillNavigation, QuickInputBar, InputBarHelpModal } from '@manacore/shared-ui'; import { SplitPaneContainer, setSplitPanelContext, @@ -150,6 +150,42 @@ let isCollapsed = $state(false); let isToolbarCollapsed = $state(true); // Default to collapsed - FAB next to InputBar + // InputBar help modal state + let helpModalOpen = $state(false); + let helpModalMode = $state<'shortcuts' | 'syntax'>('shortcuts'); + + function handleShowShortcuts() { + helpModalMode = 'shortcuts'; + helpModalOpen = true; + } + + function handleShowSyntaxHelp() { + helpModalMode = 'syntax'; + helpModalOpen = true; + } + + function handleCloseHelpModal() { + helpModalOpen = false; + } + + // Default calendar for InputBar quick create + let selectedDefaultCalendarId = $derived( + calendarsStore.calendars.find((c) => c.isDefault)?.id || calendarsStore.calendars[0]?.id + ); + + function handleDefaultCalendarChange(id: string) { + // Update the default calendar via API + calendarsStore.setAsDefault(id); + } + + // Calendar options for InputBar context menu + let calendarOptions = $derived( + calendarsStore.calendars.map((c) => ({ + id: c.id, + label: c.name, + })) + ); + // Use theme store's isDark directly let isDark = $derived(theme.isDark); @@ -431,7 +467,6 @@ onParseCreate={handleParseCreate} createText="Erstellen" appIcon="calendar" - autoFocus={true} bottomOffset={isSidebarMode ? '0px' : showCalendarToolbar && !isToolbarCollapsed @@ -439,6 +474,12 @@ : '70px'} hasFabRight={showCalendarToolbar && !isSidebarMode} hasFabLeft={showCalendarToolbar && !isSidebarMode && settingsStore.dateStripCollapsed} + defaultOptions={calendarOptions} + selectedDefaultId={selectedDefaultCalendarId} + defaultOptionLabel="Standard-Kalender" + onDefaultChange={handleDefaultCalendarChange} + onShowShortcuts={handleShowShortcuts} + onShowSyntaxHelp={handleShowSyntaxHelp} /> @@ -446,6 +487,9 @@ + + + diff --git a/packages/shared-ui/src/quick-input/index.ts b/packages/shared-ui/src/quick-input/index.ts index 74a6114bb..2d054ccd7 100644 --- a/packages/shared-ui/src/quick-input/index.ts +++ b/packages/shared-ui/src/quick-input/index.ts @@ -1,4 +1,28 @@ export { default as InputBar } from './InputBar.svelte'; // Alias for backwards compatibility 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'; + +// Recent input history (tags, references) +export { + getRecentTags, + getRecentReferences, + addRecentTag, + addRecentReference, + extractAndSaveFromInput, + clearRecentHistory, + createRecentInputHistoryStore, +} from './recentInputHistory'; + +// InputBar settings +export { + loadInputBarSettings, + saveInputBarSettings, + updateInputBarSetting, + resetInputBarSettings, + createInputBarSettingsStore, + getInputBarSettingsStore, +} from './inputBarSettings.svelte'; +export type { InputBarSettings } from './inputBarSettings.svelte'; diff --git a/packages/shared-ui/src/quick-input/inputBarSettings.svelte.ts b/packages/shared-ui/src/quick-input/inputBarSettings.svelte.ts new file mode 100644 index 000000000..ae6a85425 --- /dev/null +++ b/packages/shared-ui/src/quick-input/inputBarSettings.svelte.ts @@ -0,0 +1,127 @@ +/** + * InputBar Settings Store + * + * Persisted settings for InputBar behavior and appearance. + * Stored in localStorage for cross-session retention. + */ + +const STORAGE_KEY = 'inputbar-settings'; + +export interface InputBarSettings { + /** Enable syntax highlighting for #tags, @refs, dates, etc. */ + syntaxHighlighting: boolean; + /** Auto-focus InputBar on page load */ + autoFocus: boolean; +} + +const DEFAULT_SETTINGS: InputBarSettings = { + syntaxHighlighting: true, + autoFocus: true, +}; + +/** + * Load settings from localStorage + */ +export function loadInputBarSettings(): InputBarSettings { + if (typeof window === 'undefined') return { ...DEFAULT_SETTINGS }; + + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + return { ...DEFAULT_SETTINGS, ...parsed }; + } + } catch { + // Ignore parse errors + } + + return { ...DEFAULT_SETTINGS }; +} + +/** + * Save settings to localStorage + */ +export function saveInputBarSettings(settings: InputBarSettings): void { + if (typeof window === 'undefined') return; + + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + } catch { + // Ignore storage errors + } +} + +/** + * Update a single setting + */ +export function updateInputBarSetting( + key: K, + value: InputBarSettings[K] +): InputBarSettings { + const current = loadInputBarSettings(); + const updated = { ...current, [key]: value }; + saveInputBarSettings(updated); + return updated; +} + +/** + * Reset settings to defaults + */ +export function resetInputBarSettings(): InputBarSettings { + saveInputBarSettings(DEFAULT_SETTINGS); + return { ...DEFAULT_SETTINGS }; +} + +/** + * Create a reactive Svelte 5 store for InputBar settings + */ +export function createInputBarSettingsStore() { + let settings = $state(loadInputBarSettings()); + + function refresh() { + settings = loadInputBarSettings(); + } + + function set(key: K, value: InputBarSettings[K]) { + settings = updateInputBarSetting(key, value); + } + + function toggle(key: keyof InputBarSettings) { + if (typeof settings[key] === 'boolean') { + set(key, !settings[key] as InputBarSettings[typeof key]); + } + } + + function reset() { + settings = resetInputBarSettings(); + } + + return { + get settings() { + return settings; + }, + get syntaxHighlighting() { + return settings.syntaxHighlighting; + }, + get autoFocus() { + return settings.autoFocus; + }, + set, + toggle, + reset, + refresh, + }; +} + +// Global singleton store instance +let globalStore: ReturnType | null = null; + +/** + * Get the global InputBar settings store instance + */ +export function getInputBarSettingsStore() { + if (!globalStore) { + globalStore = createInputBarSettingsStore(); + } + return globalStore; +} diff --git a/packages/shared-ui/src/quick-input/recentInputHistory.ts b/packages/shared-ui/src/quick-input/recentInputHistory.ts new file mode 100644 index 000000000..1805dce33 --- /dev/null +++ b/packages/shared-ui/src/quick-input/recentInputHistory.ts @@ -0,0 +1,167 @@ +/** + * Recent Input History Store + * + * Tracks recently used tags (#) and references (@) for quick access in the InputBar context menu. + * Persists to localStorage for cross-session retention. + */ + +const STORAGE_KEY_TAGS = 'inputbar-recent-tags'; +const STORAGE_KEY_REFS = 'inputbar-recent-references'; +const MAX_ITEMS = 10; + +/** + * Get recent tags from localStorage + */ +export function getRecentTags(): string[] { + if (typeof window === 'undefined') return []; + try { + const stored = localStorage.getItem(STORAGE_KEY_TAGS); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } +} + +/** + * Get recent references from localStorage + */ +export function getRecentReferences(): string[] { + if (typeof window === 'undefined') return []; + try { + const stored = localStorage.getItem(STORAGE_KEY_REFS); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } +} + +/** + * Add a tag to recent history + * @param tag - The tag to add (with or without #) + */ +export function addRecentTag(tag: string): void { + if (typeof window === 'undefined') return; + + // Normalize tag (ensure it starts with #) + const normalizedTag = tag.startsWith('#') ? tag : `#${tag}`; + + try { + const current = getRecentTags(); + // Remove if already exists (will be re-added at front) + const filtered = current.filter((t) => t.toLowerCase() !== normalizedTag.toLowerCase()); + // Add to front, limit to MAX_ITEMS + const updated = [normalizedTag, ...filtered].slice(0, MAX_ITEMS); + localStorage.setItem(STORAGE_KEY_TAGS, JSON.stringify(updated)); + } catch { + // Ignore storage errors + } +} + +/** + * Add a reference to recent history + * @param reference - The reference to add (with or without @) + */ +export function addRecentReference(reference: string): void { + if (typeof window === 'undefined') return; + + // Normalize reference (ensure it starts with @) + const normalizedRef = reference.startsWith('@') ? reference : `@${reference}`; + + try { + const current = getRecentReferences(); + // Remove if already exists (will be re-added at front) + const filtered = current.filter((r) => r.toLowerCase() !== normalizedRef.toLowerCase()); + // Add to front, limit to MAX_ITEMS + const updated = [normalizedRef, ...filtered].slice(0, MAX_ITEMS); + localStorage.setItem(STORAGE_KEY_REFS, JSON.stringify(updated)); + } catch { + // Ignore storage errors + } +} + +/** + * Extract and save tags and references from input text + * Call this when user creates an item to track their usage patterns + * @param text - The input text to parse + */ +export function extractAndSaveFromInput(text: string): void { + if (!text) return; + + // Extract tags (#word) + const tagMatches = text.match(/#\w+/g); + if (tagMatches) { + tagMatches.forEach((tag) => addRecentTag(tag)); + } + + // Extract references (@word) + const refMatches = text.match(/@\w+/g); + if (refMatches) { + refMatches.forEach((ref) => addRecentReference(ref)); + } +} + +/** + * Clear all recent history + */ +export function clearRecentHistory(): void { + if (typeof window === 'undefined') return; + try { + localStorage.removeItem(STORAGE_KEY_TAGS); + localStorage.removeItem(STORAGE_KEY_REFS); + } catch { + // Ignore storage errors + } +} + +/** + * Create a reactive store for use in Svelte 5 components + * Returns reactive state that updates when history changes + */ +export function createRecentInputHistoryStore() { + let tags = $state(getRecentTags()); + let references = $state(getRecentReferences()); + + // Refresh from localStorage + function refresh() { + tags = getRecentTags(); + references = getRecentReferences(); + } + + // Add tag and refresh + function addTag(tag: string) { + addRecentTag(tag); + refresh(); + } + + // Add reference and refresh + function addReference(ref: string) { + addRecentReference(ref); + refresh(); + } + + // Extract from text and refresh + function extractAndSave(text: string) { + extractAndSaveFromInput(text); + refresh(); + } + + // Clear and refresh + function clear() { + clearRecentHistory(); + refresh(); + } + + return { + get tags() { + return tags; + }, + get references() { + return references; + }, + addTag, + addReference, + extractAndSave, + clear, + refresh, + }; +}