diff --git a/memoro/apps/web/src/lib/components/LanguageSelector.svelte b/memoro/apps/web/src/lib/components/LanguageSelector.svelte index 8789a70e6..6f47e1dde 100644 --- a/memoro/apps/web/src/lib/components/LanguageSelector.svelte +++ b/memoro/apps/web/src/lib/components/LanguageSelector.svelte @@ -1,234 +1,35 @@ -
- - - {#if isOpen} - - {/if} -
- - + diff --git a/memoro/apps/web/src/lib/utils/formatters.ts b/memoro/apps/web/src/lib/utils/formatters.ts index b8d01cb1e..c4416ef18 100644 --- a/memoro/apps/web/src/lib/utils/formatters.ts +++ b/memoro/apps/web/src/lib/utils/formatters.ts @@ -1,13 +1,33 @@ -import { format, isToday, isYesterday } from 'date-fns'; -import { de } from 'date-fns/locale'; +/** + * Memoro Formatters + * Re-exports shared utilities and adds Memoro-specific helpers + */ + +// Re-export date/time utilities from shared package +export { + formatTimestamp, + formatShortDate, + formatRelativeTime, + isToday, + isYesterday, + type LocaleKey +} from '@manacore/shared-utils'; + +// Re-export format utilities from shared package (with explicit names for clarity) +export { + formatDuration as formatDurationCompact, + parseDuration, + formatDurationWithUnits, + formatDurationHumanReadable +} from '@manacore/shared-utils'; /** - * Format duration from seconds to human-readable string with units + * Format duration from seconds to human-readable string with German units * - * Examples: - * - 45 seconds → "45 Sekunden" - * - 90 seconds → "1:30 Minuten" - * - 3665 seconds → "1:01:05 Stunden" + * This is the Memoro-specific format that displays: + * - "45 Sekunden" for seconds only + * - "1:30 Minuten" for minutes and seconds + * - "1:01:05 Stunden" for hours, minutes, and seconds */ export function formatDuration(durationSeconds: number): string { if (!durationSeconds || durationSeconds === 0) return '0 Sekunden'; @@ -36,25 +56,3 @@ export function formatDuration(durationSeconds: number): string { export function getMemooDuration(memo: any): number { return memo.source?.duration_seconds || memo.source?.duration || 0; } - -/** - * Format timestamp to localized German format - * - * Examples: - * - Today → "Heute, 14:30" - * - Yesterday → "Gestern, 14:30" - * - Other → "15. März 2024, 14:30" - */ -export function formatTimestamp(date: Date | string): string { - const dateObj = typeof date === 'string' ? new Date(date) : date; - - if (isToday(dateObj)) { - return `Heute, ${format(dateObj, 'HH:mm')}`; - } - - if (isYesterday(dateObj)) { - return `Gestern, ${format(dateObj, 'HH:mm')}`; - } - - return format(dateObj, 'd. MMMM yyyy, HH:mm', { locale: de }); -} diff --git a/memoro/apps/web/src/lib/utils/indexedDBCache.ts b/memoro/apps/web/src/lib/utils/indexedDBCache.ts index 8640364b7..536d05078 100644 --- a/memoro/apps/web/src/lib/utils/indexedDBCache.ts +++ b/memoro/apps/web/src/lib/utils/indexedDBCache.ts @@ -1,155 +1,23 @@ /** - * IndexedDB Cache for persistent storage of signed URLs - * Survives page reloads and provides better performance + * Memoro IndexedDB Cache + * Uses shared cache utilities with Memoro-specific configuration */ -const DB_NAME = 'memoro-cache'; -const DB_VERSION = 1; -const STORE_NAME = 'audio-urls'; +import { createCache } from '@manacore/shared-utils'; -interface CachedUrl { - id: string; - url: string; - expires: number; - createdAt: number; -} +// Create Memoro-specific URL cache +const memoroUrlCache = createCache({ + dbName: 'memoro-cache', + storeName: 'audio-urls', + version: 1 +}); -let dbPromise: Promise | null = null; +// Re-export with Memoro's original function names for backward compatibility +export const getCachedUrl = memoroUrlCache.get.bind(memoroUrlCache); +export const setCachedUrl = memoroUrlCache.set.bind(memoroUrlCache); +export const deleteCachedUrl = memoroUrlCache.delete.bind(memoroUrlCache); +export const cleanupExpiredUrls = memoroUrlCache.cleanupExpired.bind(memoroUrlCache); +export const clearAllCachedUrls = memoroUrlCache.clear.bind(memoroUrlCache); -/** - * Initialize IndexedDB connection - */ -function getDB(): Promise { - if (dbPromise) return dbPromise; - - dbPromise = new Promise((resolve, reject) => { - if (typeof indexedDB === 'undefined') { - reject(new Error('IndexedDB not available')); - return; - } - - const request = indexedDB.open(DB_NAME, DB_VERSION); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - - // Create store if it doesn't exist - if (!db.objectStoreNames.contains(STORE_NAME)) { - const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' }); - store.createIndex('expires', 'expires', { unique: false }); - } - }; - }); - - return dbPromise; -} - -/** - * Get cached URL from IndexedDB - */ -export async function getCachedUrl(id: string): Promise { - try { - const db = await getDB(); - const transaction = db.transaction(STORE_NAME, 'readonly'); - const store = transaction.objectStore(STORE_NAME); - - return new Promise((resolve) => { - const request = store.get(id); - - request.onsuccess = () => { - const result = request.result as CachedUrl | undefined; - if (result && Date.now() < result.expires) { - resolve(result.url); - } else { - resolve(null); - } - }; - - request.onerror = () => { - console.error('Error getting cached URL:', request.error); - resolve(null); - }; - }); - } catch { - return null; - } -} - -/** - * Store URL in IndexedDB cache - */ -export async function setCachedUrl(id: string, url: string, expiresInMs: number): Promise { - try { - const db = await getDB(); - const transaction = db.transaction(STORE_NAME, 'readwrite'); - const store = transaction.objectStore(STORE_NAME); - - const entry: CachedUrl = { - id, - url, - expires: Date.now() + expiresInMs, - createdAt: Date.now() - }; - - store.put(entry); - } catch (error) { - console.error('Error caching URL:', error); - } -} - -/** - * Delete cached URL from IndexedDB - */ -export async function deleteCachedUrl(id: string): Promise { - try { - const db = await getDB(); - const transaction = db.transaction(STORE_NAME, 'readwrite'); - const store = transaction.objectStore(STORE_NAME); - store.delete(id); - } catch (error) { - console.error('Error deleting cached URL:', error); - } -} - -/** - * Clean up expired entries from IndexedDB - * Should be called periodically - */ -export async function cleanupExpiredUrls(): Promise { - try { - const db = await getDB(); - const transaction = db.transaction(STORE_NAME, 'readwrite'); - const store = transaction.objectStore(STORE_NAME); - const index = store.index('expires'); - const now = Date.now(); - - const request = index.openCursor(IDBKeyRange.upperBound(now)); - - request.onsuccess = (event) => { - const cursor = (event.target as IDBRequest).result; - if (cursor) { - cursor.delete(); - cursor.continue(); - } - }; - } catch (error) { - console.error('Error cleaning up expired URLs:', error); - } -} - -/** - * Clear all cached URLs - */ -export async function clearAllCachedUrls(): Promise { - try { - const db = await getDB(); - const transaction = db.transaction(STORE_NAME, 'readwrite'); - const store = transaction.objectStore(STORE_NAME); - store.clear(); - } catch (error) { - console.error('Error clearing cache:', error); - } -} +// Also export the cache instance for advanced usage +export { memoroUrlCache }; diff --git a/memoro/apps/web/src/lib/utils/keyboardShortcuts.ts b/memoro/apps/web/src/lib/utils/keyboardShortcuts.ts index 15894b5e1..8884d6ad9 100644 --- a/memoro/apps/web/src/lib/utils/keyboardShortcuts.ts +++ b/memoro/apps/web/src/lib/utils/keyboardShortcuts.ts @@ -1,79 +1,26 @@ /** - * Keyboard Shortcuts Utility - * Provides centralized keyboard shortcut handling for the application + * Memoro Keyboard Shortcuts + * Re-exports shared utilities and adds Memoro-specific shortcuts */ -export interface ShortcutAction { - key: string; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; - meta?: boolean; // Command key on Mac - description: string; - action: () => void; - preventDefault?: boolean; -} +// Re-export all keyboard shortcut utilities from shared package +export { + type ShortcutAction, + type ShortcutGroup, + matchesShortcut, + formatShortcut, + formatShortcutMac, + createShortcutHandler, + createShortcuts, + shortcuts, + isMac, + getPlatformShortcut +} from '@manacore/shared-utils'; -export interface ShortcutGroup { - name: string; - shortcuts: ShortcutAction[]; -} +import type { ShortcutAction, ShortcutGroup } from '@manacore/shared-utils'; /** - * Check if a keyboard event matches a shortcut - */ -export function matchesShortcut(event: KeyboardEvent, shortcut: ShortcutAction): boolean { - const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase(); - const ctrlMatches = shortcut.ctrl ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey; - const shiftMatches = shortcut.shift ? event.shiftKey : !event.shiftKey; - const altMatches = shortcut.alt ? event.altKey : !event.altKey; - - return keyMatches && ctrlMatches && shiftMatches && altMatches; -} - -/** - * Format shortcut for display - */ -export function formatShortcut(shortcut: ShortcutAction): string { - const parts: string[] = []; - - if (shortcut.ctrl) parts.push('Ctrl'); - if (shortcut.shift) parts.push('Shift'); - if (shortcut.alt) parts.push('Alt'); - parts.push(shortcut.key.toUpperCase()); - - return parts.join('+'); -} - -/** - * Create keyboard shortcut handler - */ -export function createShortcutHandler(shortcuts: ShortcutAction[]) { - return (event: KeyboardEvent) => { - // Don't handle shortcuts if user is typing in an input - const target = event.target as HTMLElement; - if ( - target.tagName === 'INPUT' || - target.tagName === 'TEXTAREA' || - target.isContentEditable - ) { - return; - } - - for (const shortcut of shortcuts) { - if (matchesShortcut(event, shortcut)) { - if (shortcut.preventDefault !== false) { - event.preventDefault(); - } - shortcut.action(); - break; - } - } - }; -} - -/** - * Default memo panel shortcuts + * Memoro-specific memo panel shortcuts */ export function getMemoPanelShortcuts(actions: { onEdit?: () => void; @@ -212,23 +159,3 @@ export function getMemoPanelShortcuts(actions: { return shortcuts; } - -/** - * Svelte action for keyboard shortcuts - */ -export function shortcuts(node: HTMLElement, shortcutActions: ShortcutAction[]) { - const handler = createShortcutHandler(shortcutActions); - - node.addEventListener('keydown', handler); - - return { - destroy() { - node.removeEventListener('keydown', handler); - }, - update(newShortcutActions: ShortcutAction[]) { - node.removeEventListener('keydown', handler); - const newHandler = createShortcutHandler(newShortcutActions); - node.addEventListener('keydown', newHandler); - } - }; -} diff --git a/packages/shared-i18n/package.json b/packages/shared-i18n/package.json index 03c2b5e19..febc021be 100644 --- a/packages/shared-i18n/package.json +++ b/packages/shared-i18n/package.json @@ -15,6 +15,7 @@ "type-check": "tsc --noEmit" }, "devDependencies": { + "svelte": "^5.43.14", "typescript": "^5.7.3" } } diff --git a/packages/shared-i18n/src/components/LanguageSelector.svelte b/packages/shared-i18n/src/components/LanguageSelector.svelte new file mode 100644 index 000000000..0fccc1230 --- /dev/null +++ b/packages/shared-i18n/src/components/LanguageSelector.svelte @@ -0,0 +1,241 @@ + + +
+ + + {#if isOpen} + + {/if} +
+ + diff --git a/packages/shared-i18n/src/components/index.ts b/packages/shared-i18n/src/components/index.ts new file mode 100644 index 000000000..d20d8e20a --- /dev/null +++ b/packages/shared-i18n/src/components/index.ts @@ -0,0 +1 @@ +export { default as LanguageSelector } from './LanguageSelector.svelte'; diff --git a/packages/shared-i18n/src/index.ts b/packages/shared-i18n/src/index.ts index ffc2acc42..288aeb1e7 100644 --- a/packages/shared-i18n/src/index.ts +++ b/packages/shared-i18n/src/index.ts @@ -44,3 +44,6 @@ export { getCommonTranslations, mergeWithCommon, } from './translations/common'; + +// Components +export { LanguageSelector } from './components'; diff --git a/packages/shared-i18n/tsconfig.json b/packages/shared-i18n/tsconfig.json index 121a61a7f..7da622336 100644 --- a/packages/shared-i18n/tsconfig.json +++ b/packages/shared-i18n/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "ES2022", + "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", - "lib": ["ES2022"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -12,7 +12,8 @@ "declarationMap": true, "resolveJsonModule": true, "isolatedModules": true, - "noEmit": true + "noEmit": true, + "types": ["svelte"] }, "include": ["src/**/*"], "exclude": ["node_modules"] diff --git a/packages/shared-utils/src/cache.ts b/packages/shared-utils/src/cache.ts new file mode 100644 index 000000000..1d1a46d3c --- /dev/null +++ b/packages/shared-utils/src/cache.ts @@ -0,0 +1,205 @@ +/** + * IndexedDB Cache Utility + * Provides persistent storage for cached data like signed URLs + * Survives page reloads and provides better performance than localStorage + */ + +interface CacheEntry { + id: string; + data: T; + expires: number; + createdAt: number; +} + +interface CacheConfig { + /** Database name */ + dbName: string; + /** Store name */ + storeName: string; + /** Database version */ + version?: number; +} + +let dbPromises: Map> = new Map(); + +/** + * Initialize IndexedDB connection + */ +function getDB(config: CacheConfig): Promise { + const key = `${config.dbName}:${config.storeName}`; + + if (dbPromises.has(key)) { + return dbPromises.get(key)!; + } + + const promise = new Promise((resolve, reject) => { + if (typeof indexedDB === 'undefined') { + reject(new Error('IndexedDB not available')); + return; + } + + const request = indexedDB.open(config.dbName, config.version || 1); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + + // Create store if it doesn't exist + if (!db.objectStoreNames.contains(config.storeName)) { + const store = db.createObjectStore(config.storeName, { keyPath: 'id' }); + store.createIndex('expires', 'expires', { unique: false }); + } + }; + }); + + dbPromises.set(key, promise); + return promise; +} + +/** + * Create a cache instance for a specific database/store + */ +export function createCache(config: CacheConfig) { + return { + /** + * Get cached data by ID + * Returns null if not found or expired + */ + async get(id: string): Promise { + try { + const db = await getDB(config); + const transaction = db.transaction(config.storeName, 'readonly'); + const store = transaction.objectStore(config.storeName); + + return new Promise((resolve) => { + const request = store.get(id); + + request.onsuccess = () => { + const result = request.result as CacheEntry | undefined; + if (result && Date.now() < result.expires) { + resolve(result.data); + } else { + resolve(null); + } + }; + + request.onerror = () => { + console.error('Error getting cached data:', request.error); + resolve(null); + }; + }); + } catch { + return null; + } + }, + + /** + * Store data in cache + * @param id - Unique identifier for the cached data + * @param data - Data to cache + * @param expiresInMs - Time until expiration in milliseconds + */ + async set(id: string, data: T, expiresInMs: number): Promise { + try { + const db = await getDB(config); + const transaction = db.transaction(config.storeName, 'readwrite'); + const store = transaction.objectStore(config.storeName); + + const entry: CacheEntry = { + id, + data, + expires: Date.now() + expiresInMs, + createdAt: Date.now() + }; + + store.put(entry); + } catch (error) { + console.error('Error caching data:', error); + } + }, + + /** + * Delete cached data by ID + */ + async delete(id: string): Promise { + try { + const db = await getDB(config); + const transaction = db.transaction(config.storeName, 'readwrite'); + const store = transaction.objectStore(config.storeName); + store.delete(id); + } catch (error) { + console.error('Error deleting cached data:', error); + } + }, + + /** + * Clean up expired entries + * Should be called periodically + */ + async cleanupExpired(): Promise { + try { + const db = await getDB(config); + const transaction = db.transaction(config.storeName, 'readwrite'); + const store = transaction.objectStore(config.storeName); + const index = store.index('expires'); + const now = Date.now(); + + const request = index.openCursor(IDBKeyRange.upperBound(now)); + + request.onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result; + if (cursor) { + cursor.delete(); + cursor.continue(); + } + }; + } catch (error) { + console.error('Error cleaning up expired entries:', error); + } + }, + + /** + * Clear all cached data + */ + async clear(): Promise { + try { + const db = await getDB(config); + const transaction = db.transaction(config.storeName, 'readwrite'); + const store = transaction.objectStore(config.storeName); + store.clear(); + } catch (error) { + console.error('Error clearing cache:', error); + } + }, + + /** + * Check if an item exists and is not expired + */ + async has(id: string): Promise { + const data = await this.get(id); + return data !== null; + } + }; +} + +// Default URL cache for signed URLs (like Supabase storage URLs) +const DEFAULT_URL_CACHE_CONFIG: CacheConfig = { + dbName: 'manacore-cache', + storeName: 'urls', + version: 1 +}; + +/** + * Pre-configured cache for signed URLs + * Useful for caching Supabase storage signed URLs + */ +export const urlCache = createCache(DEFAULT_URL_CACHE_CONFIG); + +// Convenience exports for URL caching (backward compatibility) +export const getCachedUrl = urlCache.get.bind(urlCache); +export const setCachedUrl = urlCache.set.bind(urlCache); +export const deleteCachedUrl = urlCache.delete.bind(urlCache); +export const cleanupExpiredUrls = urlCache.cleanupExpired.bind(urlCache); +export const clearAllCachedUrls = urlCache.clear.bind(urlCache); diff --git a/packages/shared-utils/src/date.ts b/packages/shared-utils/src/date.ts index 3c73cea4f..5343ee844 100644 --- a/packages/shared-utils/src/date.ts +++ b/packages/shared-utils/src/date.ts @@ -10,7 +10,7 @@ const locales = { en: enUS, }; -type LocaleKey = keyof typeof locales; +export type LocaleKey = keyof typeof locales; /** * Format a date string to a readable format @@ -78,3 +78,16 @@ export function formatTimestamp( * Check if a date is today */ export { isToday, isYesterday } from 'date-fns'; + +/** + * Format timestamp as short date + * + * @param date - Date to format + * @param locale - Locale for formatting ('de' or 'en') + * @returns Formatted short date string (e.g., "15 Mar 2024" or "15. Mär. 2024") + */ +export function formatShortDate(date: Date | string, locale: LocaleKey = 'de'): string { + const dateObj = typeof date === 'string' ? parseISO(date) : date; + + return format(dateObj, 'dd MMM yyyy', { locale: locales[locale] }); +} diff --git a/packages/shared-utils/src/format.ts b/packages/shared-utils/src/format.ts index cd8da9d7c..1cc87221a 100644 --- a/packages/shared-utils/src/format.ts +++ b/packages/shared-utils/src/format.ts @@ -150,3 +150,29 @@ export function formatPercent( maximumFractionDigits: decimals, }).format(value); } + +/** + * Format duration as compact string (alias for formatDuration) + * Kept for compatibility - returns MM:SS or HH:MM:SS + */ +export const formatDurationCompact = formatDuration; + +/** + * Parse duration string to seconds + * + * @param duration - Duration string (e.g., "1:30" or "1:01:05") + * @returns Duration in seconds + */ +export function parseDuration(duration: string): number { + const parts = duration.split(':').map(Number); + + if (parts.length === 3) { + // Hours:Minutes:Seconds + return parts[0] * 3600 + parts[1] * 60 + parts[2]; + } else if (parts.length === 2) { + // Minutes:Seconds + return parts[0] * 60 + parts[1]; + } + + return parts[0] || 0; +} diff --git a/packages/shared-utils/src/index.ts b/packages/shared-utils/src/index.ts index 0c1635eff..f77937d38 100644 --- a/packages/shared-utils/src/index.ts +++ b/packages/shared-utils/src/index.ts @@ -16,3 +16,9 @@ export * from './format'; // Validation utilities export * from './validation'; + +// Keyboard shortcuts +export * from './keyboard'; + +// IndexedDB Cache +export * from './cache'; diff --git a/packages/shared-utils/src/keyboard.ts b/packages/shared-utils/src/keyboard.ts new file mode 100644 index 000000000..c3d48bb6e --- /dev/null +++ b/packages/shared-utils/src/keyboard.ts @@ -0,0 +1,244 @@ +/** + * Keyboard Shortcuts Utility + * Provides centralized keyboard shortcut handling for applications + */ + +export interface ShortcutAction { + key: string; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; + meta?: boolean; // Command key on Mac + description: string; + action: () => void; + preventDefault?: boolean; +} + +export interface ShortcutGroup { + name: string; + shortcuts: ShortcutAction[]; +} + +/** + * Check if a keyboard event matches a shortcut + */ +export function matchesShortcut(event: KeyboardEvent, shortcut: ShortcutAction): boolean { + const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase(); + const ctrlMatches = shortcut.ctrl ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey; + const shiftMatches = shortcut.shift ? event.shiftKey : !event.shiftKey; + const altMatches = shortcut.alt ? event.altKey : !event.altKey; + + return keyMatches && ctrlMatches && shiftMatches && altMatches; +} + +/** + * Format shortcut for display (e.g., "Ctrl+S") + */ +export function formatShortcut(shortcut: ShortcutAction): string { + const parts: string[] = []; + + if (shortcut.ctrl) parts.push('Ctrl'); + if (shortcut.shift) parts.push('Shift'); + if (shortcut.alt) parts.push('Alt'); + parts.push(shortcut.key.toUpperCase()); + + return parts.join('+'); +} + +/** + * Format shortcut for Mac display (e.g., "⌘S") + */ +export function formatShortcutMac(shortcut: ShortcutAction): string { + const parts: string[] = []; + + if (shortcut.ctrl) parts.push('⌘'); + if (shortcut.shift) parts.push('⇧'); + if (shortcut.alt) parts.push('⌥'); + parts.push(shortcut.key.toUpperCase()); + + return parts.join(''); +} + +/** + * Create keyboard shortcut handler + * @param shortcuts - Array of shortcuts to handle + * @param options - Options for the handler + */ +export function createShortcutHandler( + shortcuts: ShortcutAction[], + options?: { + /** Allow shortcuts in input fields */ + allowInInputs?: boolean; + } +) { + return (event: KeyboardEvent) => { + // Don't handle shortcuts if user is typing in an input (unless explicitly allowed) + if (!options?.allowInInputs) { + const target = event.target as HTMLElement; + if ( + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.isContentEditable + ) { + return; + } + } + + for (const shortcut of shortcuts) { + if (matchesShortcut(event, shortcut)) { + if (shortcut.preventDefault !== false) { + event.preventDefault(); + } + shortcut.action(); + break; + } + } + }; +} + +/** + * Create common shortcuts builder + * Helps create consistent shortcut definitions + */ +export function createShortcuts(actions: { + onSearch?: () => void; + onSave?: () => void; + onEdit?: () => void; + onCancel?: () => void; + onDelete?: () => void; + onNew?: () => void; + onCopy?: () => void; + onPaste?: () => void; + onUndo?: () => void; + onRedo?: () => void; +}): ShortcutGroup[] { + const shortcuts: ShortcutGroup[] = []; + const generalShortcuts: ShortcutAction[] = []; + + if (actions.onSearch) { + generalShortcuts.push({ + key: 'f', + ctrl: true, + description: 'Search', + action: actions.onSearch + }); + } + + if (actions.onSave) { + generalShortcuts.push({ + key: 's', + ctrl: true, + description: 'Save', + action: actions.onSave + }); + } + + if (actions.onEdit) { + generalShortcuts.push({ + key: 'e', + ctrl: true, + description: 'Edit', + action: actions.onEdit + }); + } + + if (actions.onCancel) { + generalShortcuts.push({ + key: 'Escape', + description: 'Cancel', + action: actions.onCancel + }); + } + + if (actions.onNew) { + generalShortcuts.push({ + key: 'n', + ctrl: true, + description: 'New', + action: actions.onNew + }); + } + + if (actions.onDelete) { + generalShortcuts.push({ + key: 'Delete', + ctrl: true, + description: 'Delete', + action: actions.onDelete + }); + } + + if (actions.onCopy) { + generalShortcuts.push({ + key: 'c', + ctrl: true, + shift: true, + description: 'Copy', + action: actions.onCopy + }); + } + + if (actions.onUndo) { + generalShortcuts.push({ + key: 'z', + ctrl: true, + description: 'Undo', + action: actions.onUndo + }); + } + + if (actions.onRedo) { + generalShortcuts.push({ + key: 'z', + ctrl: true, + shift: true, + description: 'Redo', + action: actions.onRedo + }); + } + + if (generalShortcuts.length > 0) { + shortcuts.push({ + name: 'General', + shortcuts: generalShortcuts + }); + } + + return shortcuts; +} + +/** + * Svelte action for keyboard shortcuts + * Usage:
+ */ +export function shortcuts(node: HTMLElement, shortcutActions: ShortcutAction[]) { + const handler = createShortcutHandler(shortcutActions); + + node.addEventListener('keydown', handler); + + return { + destroy() { + node.removeEventListener('keydown', handler); + }, + update(newShortcutActions: ShortcutAction[]) { + node.removeEventListener('keydown', handler); + const newHandler = createShortcutHandler(newShortcutActions); + node.addEventListener('keydown', newHandler); + } + }; +} + +/** + * Check if running on Mac + */ +export function isMac(): boolean { + if (typeof navigator === 'undefined') return false; + return navigator.platform.toUpperCase().indexOf('MAC') >= 0; +} + +/** + * Get platform-aware shortcut display + */ +export function getPlatformShortcut(shortcut: ShortcutAction): string { + return isMac() ? formatShortcutMac(shortcut) : formatShortcut(shortcut); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a0072205..8ad9153e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -309,7 +309,7 @@ importers: version: 17.0.7(expo@54.0.25)(react@19.1.0) expo-router: specifier: ~6.0.14 - version: 6.0.15(ja35odoob22us44jnpjqapckaa) + version: 6.0.15(evxcyavfmgswt4zg3ii4wlqsdm) expo-secure-store: specifier: ~15.0.7 version: 15.0.7(expo@54.0.25) @@ -412,7 +412,7 @@ importers: version: 7.28.5 '@testing-library/react-native': specifier: ^13.3.3 - version: 13.3.3(jest@29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + version: 13.3.3(jest@29.5.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) '@types/jest': specifier: ^29.5.12 version: 29.5.14 @@ -430,10 +430,10 @@ importers: version: 10.0.0 jest: specifier: ^29.2.1 - version: 29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + version: 29.5.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)) jest-expo: specifier: ~54.0.13 - version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(webpack@5.100.2) + version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.5.0)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(webpack@5.100.2) patch-package: specifier: ^8.0.0 version: 8.0.1 @@ -501,9 +501,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^6.2.1 version: 6.2.1(svelte@5.43.14)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) - '@tailwindcss/postcss': - specifier: ^4.1.17 - version: 4.1.17 + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) autoprefixer: specifier: ^10.4.22 version: 10.4.22(postcss@8.5.6) @@ -517,8 +517,8 @@ importers: specifier: ^4.3.3 version: 4.3.4(picomatch@4.0.3)(svelte@5.43.14)(typescript@5.9.3) tailwindcss: - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^3.4.17 + version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1012,12 +1012,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^6.2.1 version: 6.2.1(svelte@5.43.14)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) - '@tailwindcss/postcss': - specifier: ^4.1.17 - version: 4.1.17 '@tailwindcss/typography': specifier: ^0.5.19 - version: 0.5.19(tailwindcss@4.1.17) + version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) autoprefixer: specifier: ^10.4.22 version: 10.4.22(postcss@8.5.6) @@ -1031,8 +1028,8 @@ importers: specifier: ^4.3.3 version: 4.3.4(picomatch@4.0.3)(svelte@5.43.14)(typescript@5.9.3) tailwindcss: - specifier: ^4.1.17 - version: 4.1.17 + specifier: ^3.4.17 + version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1574,6 +1571,18 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/shared-branding: + devDependencies: + svelte: + specifier: ^5.0.0 + version: 5.43.14 + svelte-check: + specifier: ^4.0.0 + version: 4.3.4(picomatch@4.0.3)(svelte@5.43.14)(typescript@5.9.3) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + packages/shared-config: dependencies: zod: @@ -1586,6 +1595,9 @@ importers: packages/shared-i18n: devDependencies: + svelte: + specifier: ^5.43.14 + version: 5.43.14 typescript: specifier: ^5.7.3 version: 5.9.3 @@ -14819,7 +14831,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(nbbplg4zewzlp5oy3zff3m2jw4) + expo-router: 6.0.15(evxcyavfmgswt4zg3ii4wlqsdm) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -15782,41 +15794,6 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - '@jest/core@30.2.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': dependencies: '@jest/console': 30.2.0 @@ -17835,12 +17812,7 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) - '@tailwindcss/typography@0.5.19(tailwindcss@4.1.17)': - dependencies: - postcss-selector-parser: 6.0.10 - tailwindcss: 4.1.17 - - '@testing-library/react-native@13.3.3(jest@29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@29.5.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -17850,7 +17822,7 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest: 29.5.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)) '@testing-library/react-native@13.3.3(jest@30.2.0)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: @@ -20101,21 +20073,6 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - create-require@1.1.1: {} cross-fetch@3.2.0: @@ -20700,7 +20657,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) @@ -20774,7 +20731,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -20799,14 +20756,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -20878,7 +20835,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -21473,7 +21430,7 @@ snapshots: expo-device@8.0.9(expo@54.0.25): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) ua-parser-js: 0.7.41 expo-document-picker@14.0.7(expo@54.0.25): @@ -21528,7 +21485,7 @@ snapshots: expo-image-loader@6.0.0(expo@54.0.25): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-image-picker@17.0.8(expo@54.0.13): dependencies: @@ -21537,7 +21494,7 @@ snapshots: expo-image-picker@17.0.8(expo@54.0.25): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-image-loader: 6.0.0(expo@54.0.25) expo-image@3.0.10(expo@54.0.25)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): @@ -21604,7 +21561,7 @@ snapshots: expo-localization@17.0.7(expo@54.0.25)(react@19.1.0): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react: 19.1.0 rtl-detect: 1.1.2 @@ -21722,7 +21679,7 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(ja35odoob22us44jnpjqapckaa): + expo-router@6.0.15(evxcyavfmgswt4zg3ii4wlqsdm): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -21756,7 +21713,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@18.3.7(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@29.5.0)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -21866,7 +21823,7 @@ snapshots: expo-secure-store@15.0.7(expo@54.0.25): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-server@1.0.4: {} @@ -21877,7 +21834,7 @@ snapshots: expo-splash-screen@31.0.11(expo@54.0.25): dependencies: '@expo/prebuild-config': 54.0.6(expo@54.0.25) - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - supports-color @@ -23486,25 +23443,6 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest-cli@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) @@ -23586,37 +23524,6 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 - ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - jest-config@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -23722,7 +23629,7 @@ snapshots: jest-util: 30.2.0 jest-validate: 30.2.0 - jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(webpack@5.100.2): + jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@29.5.0)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(webpack@5.100.2): dependencies: '@expo/config': 12.0.10 '@expo/json-file': 10.0.7 @@ -23733,7 +23640,7 @@ snapshots: jest-environment-jsdom: 29.7.0 jest-snapshot: 29.7.0 jest-watch-select-projects: 2.0.0 - jest-watch-typeahead: 2.2.1(jest@29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))) + jest-watch-typeahead: 2.2.1(jest@29.5.0) json5: 2.2.3 lodash: 4.17.21 react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) @@ -24094,11 +24001,11 @@ snapshots: chalk: 3.0.0 prompts: 2.4.2 - jest-watch-typeahead@2.2.1(jest@29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))): + jest-watch-typeahead@2.2.1(jest@29.5.0): dependencies: ansi-escapes: 6.2.1 chalk: 4.1.2 - jest: 29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest: 29.5.0(@types/node@18.15.11)(ts-node@10.9.2(@types/node@18.15.11)(typescript@5.9.3)) jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -24160,18 +24067,6 @@ snapshots: - supports-color - ts-node - jest@29.5.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest@30.2.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@jest/core': 30.2.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))