managarten/memoro/apps/mobile/components/atoms/Icon.tsx
Till-JS e7f5f942f3 chore: initial commit - consolidate 4 projects into monorepo
Projects included:
- maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing)
- manacore (Expo mobile + SvelteKit web + Astro landing)
- manadeck (NestJS backend + Expo mobile + SvelteKit web)
- memoro (Expo mobile + SvelteKit web + Astro landing)

This commit preserves the current state before monorepo restructuring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:38:24 +01:00

190 lines
5.2 KiB
TypeScript

import React, { useMemo } from 'react';
import { View, Pressable, PressableProps, Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import * as Haptics from 'expo-haptics';
import { useTheme } from '~/features/theme/ThemeProvider';
import { getIconMapping } from '~/features/icons/iconMapping';
// Conditionally import SymbolView only on iOS
let SymbolView: any = null;
if (Platform.OS === 'ios') {
try {
SymbolView = require('expo-symbols').SymbolView;
} catch (e) {
// expo-symbols not available, will use Ionicons
}
}
// Unterstützte Icon-Typen
type IconType = 'ionicons';
// Props für die Icon-Komponente
type IconProps = {
// Icon-Name (aus Ionicons)
name: string;
// Größe des Icons
size?: number;
// Farbe des Icons (überschreibt die Theme-Farbe)
color?: string;
// Automatisch die Theme-Farbe verwenden
useThemeColor?: boolean;
// Zusätzliche Tailwind-Klassen
className?: string;
// Icon-Typ (für zukünftige Erweiterungen)
type?: IconType;
// Als Button anzeigen
asButton?: boolean;
// Hintergrundfarbe für Button-Variante
buttonBackgroundColor?: string;
// Button-Eigenschaften, wenn asButton=true
onPress?: PressableProps['onPress'];
// Zusätzliche Tailwind-Klassen für den Button-Container
buttonClassName?: string;
// Deaktiviert den Button
disabled?: boolean;
};
/**
* Icon-Komponente
*
* Ein Wrapper für verschiedene Icon-Bibliotheken, der automatisch die Theme-Farben verwendet.
* Standardmäßig wird Ionicons verwendet.
*
* Beispiel:
* ```tsx
* <Icon name="heart" size={24} />
* <Icon name="settings-outline" size={20} color="red" />
* <Icon name="home" className="ml-2" useThemeColor />
* ```
*/
/**
* Icon-Komponente mit Memoization für bessere Performance
*
* Kann auch als Button verwendet werden, indem asButton=true gesetzt wird
* und onPress übergeben wird.
*/
export const Icon: React.FC<IconProps> = React.memo(({
name,
size = 24,
color,
useThemeColor = false,
className = '',
type = 'ionicons',
asButton = false,
buttonBackgroundColor,
onPress,
buttonClassName = '',
disabled = false,
}: IconProps) => {
const { tw, themeVariant, isDark, themeVersion } = useTheme();
// Bestimme die Farbe basierend auf den Props und dem Theme
// Verwende useMemo, um die Farbe bei Theme-Änderungen neu zu berechnen
const iconColor = useMemo(() => {
if (useThemeColor) {
// Verwende die primäre Themefarbe, wenn keine Farbe angegeben ist
return isDark
? `var(--color-dark-${themeVariant}-primary)`
: `var(--color-${themeVariant}-primary)`;
} else if (!color && className) {
// Versuche, die Farbe aus den Tailwind-Klassen zu extrahieren
const colorClass = className.match(/text-([a-z0-9-]+)/)?.[0];
if (colorClass) {
const twClass = tw(colorClass);
// Extrahiere die Farbe aus der generierten Klasse
return twClass.includes('text-dark-')
? `var(--color-${twClass.replace('text-dark-', '')})`
: `var(--color-${twClass.replace('text-', '')})`;
}
}
return color;
}, [color, className, useThemeColor, themeVariant, isDark, tw, themeVersion]);
// Rendere das Icon basierend auf dem Typ
const renderIcon = () => {
// Check if we should use SF Symbols on iOS
if (Platform.OS === 'ios' && SymbolView) {
const mapping = getIconMapping(name);
if (mapping?.sfSymbol) {
// Calculate proper scale for SF Symbols based on size
let symbolScale = 'medium';
if (size <= 16) {
symbolScale = 'small';
} else if (size >= 32) {
symbolScale = 'large';
}
return (
<SymbolView
name={mapping.sfSymbol}
scale={symbolScale}
tintColor={iconColor}
style={{ width: size, height: size }}
/>
);
}
}
// Default to Ionicons
const mapping = getIconMapping(name);
const ioniconName = mapping?.ionicon || name;
return (
<Ionicons
name={ioniconName as any}
size={size}
color={iconColor}
/>
);
};
// Handler für Button-Press mit Haptic Feedback
const handlePress = async () => {
if (!disabled && onPress) {
try {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch (error) {
console.debug('Haptic feedback error:', error);
}
onPress();
}
};
// Wenn als Button angezeigt werden soll
if (asButton) {
return (
<Pressable
onPress={handlePress}
disabled={disabled}
style={({ pressed }) => [
{
padding: 8,
opacity: pressed ? 0.7 : 1,
}
]}
>
<View
style={{
width: 36,
height: 36,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: buttonBackgroundColor
}}
>
{renderIcon()}
</View>
</Pressable>
);
}
// Standard-Icon ohne Button
return (
<View style={className ? tw(className) : undefined}>
{renderIcon()}
</View>
);
});
export default Icon as React.FC<IconProps>;