diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc64686..6b30f42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+
+- Startseite entschlackt: „Spiel starten" ist die einzige auffällige Aktion (ohne Dauer-Puls); Tägliche Challenge als dezenter Textlink darunter (ohne ⚡, Farbrahmen, Countdown); Level/Galerie/Freies Malen als einfarbige Textlinks im Footer; Statistik als eine Textzeile statt drei Emoji-Karten; Streak-Badge und schwebende Sterne entfernt — die Serie steht nur noch in Malis Begrüßung
+- Merke-Phase: dünner, einfarbiger Restzeit-Balken statt 80px-Countdown-Kreis mit Gelb/Rot-Warnfarben (war bei 3 s Anzeigezeit dauerhaft im Warnmodus)
+- App-Icon/Splash/Store-Grafik: ruhigeres, entsättigtes Hellblau `#A9DCEB` statt `#60D5FA`
+
+### Fixed
+
+- Web/PWA-Icon (`public/apple-touch-icon.png`) und OG-Bild (`public/feature-graphic.png`) zeigten noch alte bzw. abgeschnittene Motive — werden jetzt von `scripts/generate-icon-assets.js` mit erzeugt
+- `apple-touch-icon`-Link in `app/_layout.tsx` ohne `/DrawFromMemory`-Basispfad
+
### Added
- Auffindbarkeit außerhalb des Play Stores: Play-Store-Badge + deutsche Kurzbeschreibung im README, Web-only Install-Banner (`WebInstallBanner`) im sichtbaren Bereich der Startseite, deutsche Keywords/Open-Graph-/JSON-LD-Auszeichnung der Landing Page
diff --git a/app.json b/app.json
index 4f5598f..08ba95d 100644
--- a/app.json
+++ b/app.json
@@ -8,7 +8,7 @@
"icon": "./assets/icons/app-icon.png",
"userInterfaceStyle": "light",
"scheme": "merkeundmale",
- "primaryColor": "#60D5FA",
+ "primaryColor": "#A9DCEB",
"description": "Gedächtnistraining für Kinder: Merken, Malen, Lernen! Eine pädagogisch wertvolle App für spielerisches Lernen.",
"privacy": "public",
"assetBundlePatterns": ["assets/**/*"],
@@ -28,7 +28,7 @@
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/icons/adaptive-icon.png",
- "backgroundColor": "#60D5FA"
+ "backgroundColor": "#A9DCEB"
},
"package": "com.s540d.merkeundmale",
"permissions": [],
@@ -45,8 +45,8 @@
"name": "Merke und Male",
"shortName": "Merke & Male",
"description": "Gedächtnistraining für Kinder: Merken, Malen, Lernen!",
- "themeColor": "#60D5FA",
- "backgroundColor": "#60D5FA",
+ "themeColor": "#A9DCEB",
+ "backgroundColor": "#A9DCEB",
"lang": "de",
"icons": [
{
@@ -66,7 +66,7 @@
"image": "./assets/icons/splash-icon.png",
"imageWidth": 220,
"resizeMode": "contain",
- "backgroundColor": "#60D5FA"
+ "backgroundColor": "#A9DCEB"
}
],
[
diff --git a/app/+html.tsx b/app/+html.tsx
index e96fe65..8f56aff 100644
--- a/app/+html.tsx
+++ b/app/+html.tsx
@@ -29,7 +29,7 @@ export default function Root({ children }: PropsWithChildren) {
-
+
{/* Apple Touch Icon – wird als Homescreen-Icon auf iOS verwendet */}
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 221b8f2..5c6a73a 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -57,7 +57,7 @@ function RootLayoutContent() {
return (
<>
-
+
diff --git a/app/index.tsx b/app/index.tsx
index 98e9f05..a6409b8 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -1,22 +1,17 @@
import React, { useEffect, useState, useCallback } from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { AnimatedCard, PressableScale, PulseView } from '@components/AnimatedPrimitives';
+import { PressableScale } from '@components/AnimatedPrimitives';
import { useRouter, useFocusEffect } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTranslation } from '@services/i18n';
import { useTheme } from '@services/ThemeContext';
-import {
- getDailyChallengeLevel,
- getSecondsUntilMidnight,
- isTodayCompleted,
-} from '@services/DailyChallengeManager';
+import { getDailyChallengeLevel, isTodayCompleted } from '@services/DailyChallengeManager';
import { getStreakData } from '@services/StreakManager';
import Colors from '../constants/Colors';
import { Spacing, FontSize, FontWeight, FontFamily, BorderRadius } from '../constants/Layout';
import SettingsModal from '@components/SettingsModal';
import QuickStatsCards from '@components/QuickStatsCards';
-import { FloatingStars } from '@components/FloatingStars';
import OnboardingModal from '@components/OnboardingModal';
import { isOnboardingDone } from '@services/OnboardingManager';
import WebTrustFooter from '@components/WebTrustFooter';
@@ -24,9 +19,15 @@ import WebInstallBanner from '@components/WebInstallBanner';
import Mascot from '@components/Mascot';
import AgeGroupModal from '@components/AgeGroupModal';
import { isAgeGroupSelected, setAgeGroup } from '@services/AgeGroupManager';
-import { getMascotProgress, getHomeGreetingKey, type MascotUnlock } from '@services/MascotManager';
+import { getHomeGreetingKey } from '@services/MascotManager';
import type { AgeGroup } from '../types';
+const FOOTER_LINKS = [
+ { route: '/levels', labelKey: 'home.levelsButton' },
+ { route: '/gallery', labelKey: 'home.galleryButton' },
+ { route: '/creative', labelKey: 'home.creativeButton' },
+] as const;
+
export default function HomeScreen() {
const { t } = useTranslation();
const { colors } = useTheme();
@@ -35,7 +36,6 @@ export default function HomeScreen() {
const [showSettings, setShowSettings] = useState(false);
const [showOnboarding, setShowOnboarding] = useState(false);
const [showAgeGroup, setShowAgeGroup] = useState(false);
- const [mascotNextUnlock, setMascotNextUnlock] = useState(null);
const maybeShowOnboarding = useCallback(async () => {
const done = await isOnboardingDone();
@@ -59,26 +59,18 @@ export default function HomeScreen() {
};
const [dailyCompleted, setDailyCompleted] = useState(false);
- const [secondsLeft, setSecondsLeft] = useState(() => getSecondsUntilMidnight());
const [dailyLevel, setDailyLevel] = useState(() => getDailyChallengeLevel());
const [currentStreak, setCurrentStreak] = useState(0);
- // On focus: refresh all daily state and start countdown interval.
- // Interval also re-checks state every minute to handle midnight rollover
- // without requiring the user to navigate away and back.
+ // On focus: refresh daily state. The interval re-checks every minute to
+ // handle midnight rollover without navigating away and back.
useFocusEffect(
useCallback(() => {
const refresh = async () => {
- setSecondsLeft(getSecondsUntilMidnight());
setDailyLevel(getDailyChallengeLevel());
- const [completed, streakData, mascotProgress] = await Promise.all([
- isTodayCompleted(),
- getStreakData(),
- getMascotProgress(),
- ]);
+ const [completed, streakData] = await Promise.all([isTodayCompleted(), getStreakData()]);
setDailyCompleted(completed);
setCurrentStreak(streakData.currentStreak);
- setMascotNextUnlock(mascotProgress.nextUnlock);
};
refresh();
@@ -87,9 +79,6 @@ export default function HomeScreen() {
}, []),
);
- const countdownHours = Math.floor(secondsLeft / 3600);
- const countdownMinutes = Math.floor((secondsLeft % 3600) / 60);
-
return (
-
-
{/* Top bar */}
@@ -111,158 +98,90 @@ export default function HomeScreen() {
{t('home.subtitle')}
-
- {currentStreak >= 2 && (
-
-
- {t('home.streakBadge', { count: String(currentStreak) })}
-
-
- )}
- setShowSettings(true)}
- style={styles.settingsButton}
- aria-label="Settings"
- >
- ⋮
-
-
+ setShowSettings(true)}
+ style={styles.settingsButton}
+ aria-label="Settings"
+ >
+ ⋮
+
- {/* Mascot-Begrüßung — einheitlicher Fortschritts-Companion (Issue #279, 1.1) */}
+ {/* Mascot-Begrüßung — einziger Ort für die Serie (Streak) auf der Startseite */}
= 2 ? 'happy' : 'neutral'}
message={t(getHomeGreetingKey(currentStreak), { count: String(currentStreak) })}
testID="home-mascot"
/>
- {mascotNextUnlock && (
-
- {t('mascot.nextUnlock', {
- count: String(mascotNextUnlock.starsRequired),
- item: t(mascotNextUnlock.labelKey),
- })}
-
- )}
{/* Nur Web: Play-Store-Angebot im sichtbaren Bereich, nicht erst im Footer */}
- {/* Hero CTA — "Spiel starten" als dominanter zentraler Button.
- Sanfter Dauer-Puls führt das Auge zur Primäraktion (prefers-reduced-motion-aware). */}
+ {/* Hero: "Spiel starten" ist die einzige auffällige Aktion. Darunter dezent
+ die Tägliche Challenge als Textlink — ohne Emoji, Farbrahmen und Countdown. */}
-
- router.push('/game')}
- accessibilityRole="button"
- accessibilityLabel={t('home.startButton')}
- style={styles.heroCtaWrapper}
+ router.push('/game')}
+ accessibilityRole="button"
+ accessibilityLabel={t('home.startButton')}
+ style={styles.heroCtaWrapper}
+ >
+
-
- ▶
- {t('home.startButton')}
-
-
-
-
+ ▶
+ {t('home.startButton')}
+
+
- {/* Bottom section: sekundär-Aktionen + Stats — versetzt, nicht statisch gestapelt.
- Gestaffelte Eingangs-Animation (AnimatedCard-Index) + Press-Feedback (PressableScale). */}
-
- {/* Daily Challenge — breite Karte, abgesetzt von den anderen */}
-
- !dailyCompleted && router.push(`/game?level=${dailyLevel}&daily=1`)}
+ disabled={dailyCompleted}
+ accessibilityRole="button"
+ accessibilityState={{ disabled: dailyCompleted }}
+ style={styles.dailyLink}
+ >
+ !dailyCompleted && router.push(`/game?level=${dailyLevel}&daily=1`)}
- accessibilityRole="button"
- disabled={dailyCompleted}
- accessibilityState={{ disabled: dailyCompleted }}
>
- ⚡
-
-
- {t('dailyChallenge.title')}
-
-
- {dailyCompleted
- ? t('dailyChallenge.completed')
- : `${t('dailyChallenge.level', { number: String(dailyLevel) })} · ${t('dailyChallenge.countdown', { hours: String(countdownHours), minutes: String(countdownMinutes) })}`}
-
-
-
-
-
- {/* Levels + Galerie + Kreativ — Side-by-Side */}
-
- router.push('/levels')}
- accessibilityRole="button"
- accessibilityLabel={t('home.levelsButton')}
- >
-
- {t('home.levelsButton')}
-
-
-
- router.push('/gallery')}
- accessibilityRole="button"
- accessibilityLabel={t('home.galleryButton')}
- >
-
- {t('home.galleryButton')}
-
-
+ {dailyCompleted
+ ? `${t('dailyChallenge.title')} · ${t('dailyChallenge.completed')}`
+ : `${t('dailyChallenge.title')} · ${t('dailyChallenge.level', { number: String(dailyLevel) })}`}
+
+
+
- router.push('/creative')}
- accessibilityRole="button"
- accessibilityLabel={t('home.creativeButton')}
- >
-
- {t('home.creativeButton')}
-
-
-
+ {/* Footer: Sekundär-Navigation als schlichte Textlinks in einer Farbe */}
+
+
+ {FOOTER_LINKS.map((link, i) => (
+
+ {i > 0 && (
+ ·
+ )}
+ router.push(link.route)}
+ accessibilityRole="button"
+ accessibilityLabel={t(link.labelKey)}
+ style={styles.footerLink}
+ >
+
+ {t(link.labelKey)}
+
+
+
+ ))}
+
- {/* Quick Stats — dezent am unteren Rand */}
@@ -292,23 +211,6 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'flex-start',
},
- topBarRight: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: Spacing.xs,
- },
- streakBadge: {
- backgroundColor: Colors.accents.streakBadge,
- borderRadius: BorderRadius.round,
- paddingHorizontal: Spacing.sm,
- paddingVertical: 3,
- },
- streakBadgeText: {
- color: '#fff',
- fontSize: FontSize.xs,
- fontWeight: FontWeight.bold,
- fontFamily: FontFamily.bold,
- },
appTitle: {
fontSize: FontSize.xl,
fontWeight: FontWeight.bold,
@@ -332,22 +234,18 @@ const styles = StyleSheet.create({
},
mascotRow: {
marginTop: Spacing.sm,
- gap: 2,
- },
- mascotUnlockHint: {
- fontSize: FontSize.xs,
- marginLeft: 64,
},
heroSlot: {
flex: 1,
- minHeight: 140,
+ minHeight: 160,
justifyContent: 'center',
alignItems: 'center',
+ gap: Spacing.md,
},
heroCtaWrapper: {
borderRadius: BorderRadius.round,
overflow: 'hidden',
- ...Colors.shadow.large,
+ ...Colors.shadow.medium,
},
heroCtaPill: {
flexDirection: 'row',
@@ -370,56 +268,39 @@ const styles = StyleSheet.create({
color: '#fff',
letterSpacing: 0.5,
},
- bottomSection: {
- marginTop: 'auto',
- gap: Spacing.md,
- },
- secondaryRow: {
- flexDirection: 'row',
- gap: Spacing.sm,
- },
- secondaryTile: {
- flex: 1,
- paddingVertical: Spacing.md,
- paddingHorizontal: Spacing.md,
- borderRadius: BorderRadius.lg,
- borderWidth: 2,
- alignItems: 'center',
- minHeight: 52,
+ dailyLink: {
+ minHeight: 44,
justifyContent: 'center',
- ...Colors.shadow.small,
+ paddingHorizontal: Spacing.md,
},
- secondaryTileText: {
- fontSize: FontSize.md,
+ dailyLinkText: {
+ fontSize: FontSize.sm,
fontWeight: FontWeight.semibold,
fontFamily: FontFamily.semibold,
+ textAlign: 'center',
},
- dailyChallengeButton: {
- flexDirection: 'row',
+ footer: {
+ marginTop: 'auto',
+ gap: Spacing.xs,
alignItems: 'center',
- paddingVertical: Spacing.md,
- paddingHorizontal: Spacing.lg,
- borderRadius: BorderRadius.lg,
- borderWidth: 2,
- minHeight: 52,
- gap: Spacing.sm,
- ...Colors.shadow.small,
- },
- dailyChallengeCompleted: {
- opacity: 0.6,
},
- dailyChallengeEmoji: {
- fontSize: 24,
+ footerLinks: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ alignItems: 'center',
},
- dailyChallengeInfo: {
- flex: 1,
+ footerLink: {
+ minHeight: 44,
+ justifyContent: 'center',
+ paddingHorizontal: Spacing.sm,
},
- dailyChallengeTitle: {
+ footerLinkText: {
fontSize: FontSize.sm,
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.semibold,
+ fontFamily: FontFamily.semibold,
},
- dailyChallengeMeta: {
- fontSize: FontSize.xs,
- marginTop: 1,
+ footerSeparator: {
+ fontSize: FontSize.sm,
},
});
diff --git a/assets/icons/adaptive-icon-foreground.svg b/assets/icons/adaptive-icon-foreground.svg
index 8eb0a14..c3dc547 100644
--- a/assets/icons/adaptive-icon-foreground.svg
+++ b/assets/icons/adaptive-icon-foreground.svg
@@ -1,6 +1,6 @@