mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
feat: unify utilities into shared packages (Tier 1)
- Add LanguageSelector component to @manacore/shared-i18n - Add keyboard.ts to @manacore/shared-utils (shortcuts handling) - Add cache.ts to @manacore/shared-utils (IndexedDB caching) - Extend format.ts and date.ts with additional utilities - Update Memoro to use shared utilities with app-specific wrappers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7d426d57fd
commit
74ccad38d5
15 changed files with 877 additions and 647 deletions
|
|
@ -1,234 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { setLocale, supportedLocales, type SupportedLocale } from '$lib/i18n';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { LanguageSelector } from '@manacore/shared-i18n';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { theme, type ThemeVariant } from '$lib/stores/theme';
|
||||
|
||||
// Language info with flags and names
|
||||
const languages: Record<SupportedLocale, { flag: string; name: string; code: string }> = {
|
||||
de: { flag: '🇩🇪', name: 'Deutsch', code: 'DE' },
|
||||
en: { flag: '🇬🇧', name: 'English', code: 'EN' },
|
||||
fr: { flag: '🇫🇷', name: 'Français', code: 'FR' },
|
||||
it: { flag: '🇮🇹', name: 'Italiano', code: 'IT' },
|
||||
es: { flag: '🇪🇸', name: 'Español', code: 'ES' }
|
||||
};
|
||||
|
||||
let currentTheme = $derived($theme);
|
||||
let currentTheme = $derived(theme);
|
||||
let isDark = $derived(currentTheme.effectiveMode === 'dark');
|
||||
let currentLocale = $derived(($locale as SupportedLocale) || 'en');
|
||||
let isOpen = $state(false);
|
||||
let currentLocale = $derived($locale || 'en');
|
||||
|
||||
function getPrimaryColor() {
|
||||
const variantColors: Record<ThemeVariant, { light: string; dark: string }> = {
|
||||
lume: { light: '#f8d62b', dark: '#f8d62b' },
|
||||
nature: { light: '#4CAF50', dark: '#4CAF50' },
|
||||
stone: { light: '#607D8B', dark: '#78909C' },
|
||||
ocean: { light: '#039BE5', dark: '#039BE5' }
|
||||
};
|
||||
|
||||
function getPrimaryColor(): string {
|
||||
const variant = currentTheme.variant;
|
||||
if (isDark) {
|
||||
const colors = {
|
||||
lume: '#f8d62b',
|
||||
nature: '#4CAF50',
|
||||
stone: '#78909C',
|
||||
ocean: '#039BE5'
|
||||
};
|
||||
return colors[variant];
|
||||
} else {
|
||||
const colors = {
|
||||
lume: '#f8d62b',
|
||||
nature: '#4CAF50',
|
||||
stone: '#607D8B',
|
||||
ocean: '#039BE5'
|
||||
};
|
||||
return colors[variant];
|
||||
}
|
||||
const colors = variantColors[variant];
|
||||
return isDark ? colors.dark : colors.light;
|
||||
}
|
||||
|
||||
function handleLocaleChange(newLocale: SupportedLocale) {
|
||||
setLocale(newLocale);
|
||||
isOpen = false;
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
setLocale(newLocale as any);
|
||||
}
|
||||
|
||||
function toggleDropdown() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.language-selector')) {
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="language-selector">
|
||||
<button
|
||||
onclick={toggleDropdown}
|
||||
class="language-button"
|
||||
class:active={isOpen}
|
||||
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.6)'};
|
||||
border-color: {isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<span class="flag">{languages[currentLocale].flag}</span>
|
||||
<span class="code">{languages[currentLocale].code}</span>
|
||||
<svg
|
||||
class="chevron"
|
||||
class:rotate={isOpen}
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M2 4l4 4 4-4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="dropdown"
|
||||
style="background-color: {isDark ? '#1E1E1E' : '#ffffff'};
|
||||
border-color: {isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, {isDark ? '0.4' : '0.15'});"
|
||||
>
|
||||
{#each supportedLocales as lang}
|
||||
<button
|
||||
onclick={() => handleLocaleChange(lang)}
|
||||
class="dropdown-item"
|
||||
class:active={currentLocale === lang}
|
||||
style="color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<span class="flag">{languages[lang].flag}</span>
|
||||
<span class="name">{languages[lang].name}</span>
|
||||
{#if currentLocale === lang}
|
||||
<svg
|
||||
class="check"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke={getPrimaryColor()}
|
||||
>
|
||||
<path
|
||||
d="M3 8l3 3 7-7"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
<LanguageSelector
|
||||
{currentLocale}
|
||||
{supportedLocales}
|
||||
onLocaleChange={handleLocaleChange}
|
||||
{isDark}
|
||||
primaryColor={getPrimaryColor()}
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.language-selector {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.language-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.language-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.language-button.active {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.flag {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.chevron.rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .dropdown-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-item .name {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.check {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
let dbPromise: Promise<IDBDatabase> | null = null;
|
||||
|
||||
/**
|
||||
* Initialize IndexedDB connection
|
||||
*/
|
||||
function getDB(): Promise<IDBDatabase> {
|
||||
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 });
|
||||
}
|
||||
};
|
||||
// Create Memoro-specific URL cache
|
||||
const memoroUrlCache = createCache<string>({
|
||||
dbName: 'memoro-cache',
|
||||
storeName: 'audio-urls',
|
||||
version: 1
|
||||
});
|
||||
|
||||
return dbPromise;
|
||||
}
|
||||
// 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);
|
||||
|
||||
/**
|
||||
* Get cached URL from IndexedDB
|
||||
*/
|
||||
export async function getCachedUrl(id: string): Promise<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^5.43.14",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
241
packages/shared-i18n/src/components/LanguageSelector.svelte
Normal file
241
packages/shared-i18n/src/components/LanguageSelector.svelte
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<script lang="ts">
|
||||
import type { LanguageCode, LanguageInfo } from '../languages';
|
||||
import { LANGUAGES, getLanguageInfo } from '../languages';
|
||||
|
||||
interface Props {
|
||||
/** Current locale */
|
||||
currentLocale: string;
|
||||
/** Supported locales for this app */
|
||||
supportedLocales: readonly string[] | string[];
|
||||
/** Callback when locale changes */
|
||||
onLocaleChange: (locale: string) => void;
|
||||
/** Whether dark mode is active */
|
||||
isDark?: boolean;
|
||||
/** Primary color for check mark (optional) */
|
||||
primaryColor?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
currentLocale,
|
||||
supportedLocales,
|
||||
onLocaleChange,
|
||||
isDark = false,
|
||||
primaryColor = '#6366f1'
|
||||
}: Props = $props();
|
||||
|
||||
let isOpen = $state(false);
|
||||
|
||||
// Get language info with fallback
|
||||
function getLanguageDisplay(code: string): { flag: string; name: string; code: string } {
|
||||
const info = getLanguageInfo(code);
|
||||
if (info) {
|
||||
return {
|
||||
flag: info.emoji,
|
||||
name: info.nativeName,
|
||||
code: code.toUpperCase().substring(0, 2)
|
||||
};
|
||||
}
|
||||
return {
|
||||
flag: '🌐',
|
||||
name: code,
|
||||
code: code.toUpperCase().substring(0, 2)
|
||||
};
|
||||
}
|
||||
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
onLocaleChange(newLocale);
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function toggleDropdown() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.language-selector')) {
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}
|
||||
});
|
||||
|
||||
let currentLang = $derived(getLanguageDisplay(currentLocale));
|
||||
</script>
|
||||
|
||||
<div class="language-selector">
|
||||
<button
|
||||
onclick={toggleDropdown}
|
||||
class="language-button"
|
||||
class:active={isOpen}
|
||||
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.6)'};
|
||||
border-color: {isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<span class="flag">{currentLang.flag}</span>
|
||||
<span class="code">{currentLang.code}</span>
|
||||
<svg
|
||||
class="chevron"
|
||||
class:rotate={isOpen}
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M2 4l4 4 4-4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="dropdown"
|
||||
style="background-color: {isDark ? '#1E1E1E' : '#ffffff'};
|
||||
border-color: {isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, {isDark ? '0.4' : '0.15'});"
|
||||
>
|
||||
{#each supportedLocales as locale}
|
||||
{@const lang = getLanguageDisplay(locale)}
|
||||
<button
|
||||
onclick={() => handleLocaleChange(locale)}
|
||||
class="dropdown-item"
|
||||
class:active={currentLocale === locale}
|
||||
style="color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<span class="flag">{lang.flag}</span>
|
||||
<span class="name">{lang.name}</span>
|
||||
{#if currentLocale === locale}
|
||||
<svg
|
||||
class="check"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke={primaryColor}
|
||||
>
|
||||
<path
|
||||
d="M3 8l3 3 7-7"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.language-selector {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.language-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.language-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.language-button.active {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.flag {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.chevron.rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .dropdown-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-item .name {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.check {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
1
packages/shared-i18n/src/components/index.ts
Normal file
1
packages/shared-i18n/src/components/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as LanguageSelector } from './LanguageSelector.svelte';
|
||||
|
|
@ -44,3 +44,6 @@ export {
|
|||
getCommonTranslations,
|
||||
mergeWithCommon,
|
||||
} from './translations/common';
|
||||
|
||||
// Components
|
||||
export { LanguageSelector } from './components';
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
205
packages/shared-utils/src/cache.ts
Normal file
205
packages/shared-utils/src/cache.ts
Normal file
|
|
@ -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<T = string> {
|
||||
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<string, Promise<IDBDatabase>> = new Map();
|
||||
|
||||
/**
|
||||
* Initialize IndexedDB connection
|
||||
*/
|
||||
function getDB(config: CacheConfig): Promise<IDBDatabase> {
|
||||
const key = `${config.dbName}:${config.storeName}`;
|
||||
|
||||
if (dbPromises.has(key)) {
|
||||
return dbPromises.get(key)!;
|
||||
}
|
||||
|
||||
const promise = new Promise<IDBDatabase>((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<T = string>(config: CacheConfig) {
|
||||
return {
|
||||
/**
|
||||
* Get cached data by ID
|
||||
* Returns null if not found or expired
|
||||
*/
|
||||
async get(id: string): Promise<T | null> {
|
||||
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<T> | 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<void> {
|
||||
try {
|
||||
const db = await getDB(config);
|
||||
const transaction = db.transaction(config.storeName, 'readwrite');
|
||||
const store = transaction.objectStore(config.storeName);
|
||||
|
||||
const entry: CacheEntry<T> = {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<string>(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);
|
||||
|
|
@ -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] });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,3 +16,9 @@ export * from './format';
|
|||
|
||||
// Validation utilities
|
||||
export * from './validation';
|
||||
|
||||
// Keyboard shortcuts
|
||||
export * from './keyboard';
|
||||
|
||||
// IndexedDB Cache
|
||||
export * from './cache';
|
||||
|
|
|
|||
244
packages/shared-utils/src/keyboard.ts
Normal file
244
packages/shared-utils/src/keyboard.ts
Normal file
|
|
@ -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: <div use:shortcuts={shortcutActions}>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
199
pnpm-lock.yaml
generated
199
pnpm-lock.yaml
generated
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue