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 @@ diff --git a/assets/icons/app-icon-512.png b/assets/icons/app-icon-512.png index f988e1d..06f8e26 100644 Binary files a/assets/icons/app-icon-512.png and b/assets/icons/app-icon-512.png differ diff --git a/assets/icons/app-icon.png b/assets/icons/app-icon.png index 5f70c6d..1d51150 100644 Binary files a/assets/icons/app-icon.png and b/assets/icons/app-icon.png differ diff --git a/assets/icons/app-icon.svg b/assets/icons/app-icon.svg index efeee15..3955d0f 100644 --- a/assets/icons/app-icon.svg +++ b/assets/icons/app-icon.svg @@ -1,7 +1,7 @@ - + diff --git a/assets/icons/favicon.png b/assets/icons/favicon.png index 5f70c6d..1d51150 100644 Binary files a/assets/icons/favicon.png and b/assets/icons/favicon.png differ diff --git a/assets/icons/feature-graphic.png b/assets/icons/feature-graphic.png index 0976b5d..8fd1455 100644 Binary files a/assets/icons/feature-graphic.png and b/assets/icons/feature-graphic.png differ diff --git a/assets/icons/feature-graphic.svg b/assets/icons/feature-graphic.svg index 9bcf786..d11f640 100644 --- a/assets/icons/feature-graphic.svg +++ b/assets/icons/feature-graphic.svg @@ -1,12 +1,12 @@ - + - - - - + + + + @@ -84,7 +84,7 @@ - Gedächtnistraining für Kinder + Gedächtnistraining für Kinder diff --git a/assets/icons/splash-icon.svg b/assets/icons/splash-icon.svg index caf8083..8ec1c9e 100644 --- a/assets/icons/splash-icon.svg +++ b/assets/icons/splash-icon.svg @@ -1,6 +1,6 @@ diff --git a/components/QuickStatsCards.tsx b/components/QuickStatsCards.tsx index 868716b..cdb17c4 100644 --- a/components/QuickStatsCards.tsx +++ b/components/QuickStatsCards.tsx @@ -1,225 +1,59 @@ -import React, { useState, useCallback, useEffect, useRef } from 'react'; -import { View, Text, StyleSheet, useWindowDimensions } from 'react-native'; -import { useRouter, useFocusEffect } from 'expo-router'; -import { GlassCard } from '@components/AnimatedPrimitives'; +import React, { useState, useCallback } from 'react'; +import { Text, StyleSheet } from 'react-native'; +import { useFocusEffect } from 'expo-router'; import { useTheme } from '@services/ThemeContext'; import { useTranslation } from '@services/i18n'; -import { useReduceMotion } from '../utils/useReduceMotion'; import storageManager from '@services/StorageManager'; -import { getStreakData } from '@services/StreakManager'; import { getTotalLevels } from '@services/LevelManager'; -import Colors from '../constants/Colors'; -import { Spacing, FontSize, FontWeight, FontFamily, BorderRadius } from '../constants/Layout'; +import { FontSize, FontFamily } from '../constants/Layout'; interface Stats { totalStars: number; - currentStreak: number; levelsCompleted: number; } -const ZERO_STATS: Stats = { totalStars: 0, currentStreak: 0, levelsCompleted: 0 }; -const COUNTER_DURATION_MS = 600; -const COUNTER_STEPS = 20; - +/** + * Dezente Fortschrittszeile auf der Startseite ("7 Sterne · 2 von 20 Level"). + * Bewusst nur Text — keine Karten, Emoji oder Zähler-Animation, damit + * "Spiel starten" die einzige auffällige Aktion bleibt. Die Serie (Streak) + * erscheint nur noch in Malis Begrüßung. + */ export default function QuickStatsCards() { const { t } = useTranslation(); - const { theme, colors } = useTheme(); - const router = useRouter(); - const { width } = useWindowDimensions(); - const reduceMotion = useReduceMotion(); - const [stats, setStats] = useState(ZERO_STATS); - const [displayStats, setDisplayStats] = useState(ZERO_STATS); - - const glassSurface = theme === 'dark' ? Colors.glass.darkSurface : Colors.glass.lightSurface; - const glassBorder = theme === 'dark' ? Colors.glass.darkBorder : Colors.glass.lightBorder; - const glassShadow = theme === 'dark' ? Colors.glass.darkShadow : Colors.glass.lightShadow; + const { colors } = useTheme(); + const [stats, setStats] = useState(null); useFocusEffect( useCallback(() => { let mounted = true; - const load = async () => { - const [progress, streakData] = await Promise.all([ - storageManager.getProgress(), - getStreakData(), - ]); + storageManager.getProgress().then(progress => { if (!mounted) return; const totalStars = Object.values(progress.levels).reduce((sum, l) => sum + l.bestRating, 0); - setStats({ - totalStars, - currentStreak: streakData.currentStreak, - levelsCompleted: progress.totalLevelsCompleted, - }); - }; - load(); + setStats({ totalStars, levelsCompleted: progress.totalLevelsCompleted }); + }); return () => { mounted = false; }; }, []), ); - // Animate displayed values from 0 to the loaded targets - const intervalRef = useRef | null>(null); - - useEffect(() => { - if (intervalRef.current) clearInterval(intervalRef.current); - - if ( - reduceMotion || - (stats.totalStars === 0 && stats.currentStreak === 0 && stats.levelsCompleted === 0) - ) { - setDisplayStats(stats); - return; - } - - setDisplayStats(ZERO_STATS); - let step = 0; - intervalRef.current = setInterval(() => { - step++; - const t = Math.min(1, step / COUNTER_STEPS); - setDisplayStats({ - totalStars: Math.round(t * stats.totalStars), - currentStreak: Math.round(t * stats.currentStreak), - levelsCompleted: Math.round(t * stats.levelsCompleted), - }); - if (step >= COUNTER_STEPS) { - if (intervalRef.current) clearInterval(intervalRef.current); - setDisplayStats(stats); - } - }, COUNTER_DURATION_MS / COUNTER_STEPS); - - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [stats.totalStars, stats.currentStreak, stats.levelsCompleted, reduceMotion]); // eslint-disable-line react-hooks/exhaustive-deps - - // Below 340px: wrap into 2 rows; otherwise all 3 in one row - const narrow = width < 340; - const totalLevels = getTotalLevels(); + if (!stats) return null; - const cardStyle = [ - styles.card, - { backgroundColor: glassSurface, borderColor: glassBorder, borderWidth: 1.5 }, - glassShadow, - narrow ? styles.cardNarrow : styles.cardWide, - ]; - - const cards = [ - { - emoji: '⭐', - label: t('home.stats.stars'), - value: String(displayStats.totalStars), - sub: null, - onPress: () => router.push('/gallery'), - index: 0, - }, - { - emoji: '🔥', - label: t('home.stats.streak'), - value: String(displayStats.currentStreak), - sub: - stats.currentStreak > 0 - ? t(stats.currentStreak === 1 ? 'home.stats.streakDay' : 'home.stats.streakDays') - : null, - onPress: () => router.push('/levels'), - index: 1, - }, - { - emoji: '🏆', - label: t('home.stats.levels'), - value: String(displayStats.levelsCompleted), - sub: t('home.stats.levelOf', { total: String(totalLevels) }), - onPress: () => router.push('/levels'), - index: 2, - }, - ]; + const text = + `${stats.totalStars} ${t('home.stats.stars')} · ` + + `${stats.levelsCompleted} ${t('home.stats.levelOf', { total: String(getTotalLevels()) })} ${t('home.stats.levels')}`; return ( - - {cards.map(card => ( - - {card.emoji} - - {card.value} - - - {card.label} - - {card.sub !== null && ( - - {card.sub} - - )} - - ))} - + + {text} + ); } const styles = StyleSheet.create({ - row: { - flexDirection: 'row', - gap: Spacing.sm, - justifyContent: 'space-between', - }, - rowWrap: { - flexWrap: 'wrap', - }, - card: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: Spacing.sm, - paddingHorizontal: Spacing.xs, - borderRadius: BorderRadius.xl, - minWidth: 80, - gap: 1, - }, - cardWide: { - minWidth: 0, - }, - cardNarrow: { - minWidth: '45%', - flexGrow: 1, - }, - emoji: { - fontSize: 20, - }, - value: { - fontSize: FontSize.md, - fontWeight: FontWeight.bold, - fontFamily: FontFamily.bold, - lineHeight: 22, - }, - label: { + line: { fontSize: FontSize.xs, - fontWeight: FontWeight.semibold, fontFamily: FontFamily.semibold, textAlign: 'center', }, - sub: { - fontSize: 10, - textAlign: 'center', - marginTop: 1, - }, }); diff --git a/components/__tests__/GamePhaseComponents.test.tsx b/components/__tests__/GamePhaseComponents.test.tsx index 438680f..fa508ac 100644 --- a/components/__tests__/GamePhaseComponents.test.tsx +++ b/components/__tests__/GamePhaseComponents.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react-native'; +import { View } from 'react-native'; // ─── Shared mocks ──────────────────────────────────────────────────────────── @@ -100,23 +101,6 @@ jest.mock('../../components/AnimatedPrimitives', () => { }; }); -jest.mock('../../components/game/TimerArc', () => { - const { View, Text } = require('react-native'); - return { - __esModule: true, - default: ({ timeRemaining }: any) => ( - - {String(Math.max(0, Math.ceil(timeRemaining)))} - - ), - TimerArc: ({ timeRemaining }: any) => ( - - {String(Math.max(0, Math.ceil(timeRemaining)))} - - ), - }; -}); - // ─── MemorizePhase ──────────────────────────────────────────────────────────── import MemorizePhase from '../../components/game/MemorizePhase'; @@ -151,12 +135,15 @@ describe('MemorizePhase', () => { revealStep: 0, }; - it('renders timer arc countdown value', () => { + it('renders a quiet timer bar instead of a big countdown number', () => { const { UNSAFE_getAllByType } = render( , ); + const bar = UNSAFE_getAllByType(View).find((n: any) => n.props.testID === 'timer-bar'); + expect(bar).toBeDefined(); + expect(bar!.props.accessibilityValue).toEqual({ min: 0, max: 5, now: 3 }); const texts = getAllTexts(UNSAFE_getAllByType); - expect(texts).toContain('3'); + expect(texts).not.toContain('3'); }); it('renders title translation key', () => { diff --git a/components/__tests__/GameScreen.test.tsx b/components/__tests__/GameScreen.test.tsx index dd6dea9..a5c51fa 100644 --- a/components/__tests__/GameScreen.test.tsx +++ b/components/__tests__/GameScreen.test.tsx @@ -163,13 +163,9 @@ jest.mock('../../components/ConfettiBurst', () => { return { __esModule: true, default: () => }; }); -jest.mock('../../components/game/TimerArc', () => { +jest.mock('../../components/game/TimerBar', () => { const { View } = require('react-native'); - return { - __esModule: true, - default: () => , - TimerArc: () => , - }; + return { __esModule: true, default: () => }; }); jest.mock('react-native-reanimated', () => { diff --git a/components/__tests__/QuickStatsCards.test.tsx b/components/__tests__/QuickStatsCards.test.tsx index 1c947df..e24e3b6 100644 --- a/components/__tests__/QuickStatsCards.test.tsx +++ b/components/__tests__/QuickStatsCards.test.tsx @@ -3,7 +3,6 @@ import { render, act } from '@testing-library/react-native'; import { Text } from 'react-native'; jest.mock('expo-router', () => ({ - useRouter: () => ({ push: jest.fn() }), useFocusEffect: (cb: () => (() => void) | void) => { const { useEffect } = require('react'); useEffect(() => { @@ -29,11 +28,6 @@ jest.mock('../../services/StorageManager', () => ({ }, })); -jest.mock('../../services/StreakManager', () => ({ - getStreakData: () => - Promise.resolve({ currentStreak: 5, longestStreak: 7, lastPlayedDate: '2026-05-23' }), -})); - jest.mock('../../services/LevelManager', () => ({ getTotalLevels: () => 10, })); @@ -58,31 +52,6 @@ jest.mock('../../services/i18n', () => ({ }), })); -jest.mock('react-native-reanimated', () => { - const { View, Text: RNText } = require('react-native'); - return { - __esModule: true, - default: { - View, - Text: RNText, - createAnimatedComponent: (c: any) => c, - call: () => {}, - }, - useSharedValue: (v: any) => ({ value: v }), - useAnimatedStyle: (_fn: any) => ({}), - withSpring: (v: any) => v, - withTiming: (v: any) => v, - withDelay: (_d: any, v: any) => v, - withSequence: (...args: any[]) => args[args.length - 1], - Easing: { out: () => () => {}, ease: {} }, - runOnJS: (fn: any) => fn, - }; -}); - -jest.mock('../../utils/useReduceMotion', () => ({ - useReduceMotion: () => false, -})); - const getAllTexts = (getAllByType: (type: any) => any[]) => getAllByType(Text) .map((n: any) => n.props.children) @@ -91,54 +60,19 @@ const getAllTexts = (getAllByType: (type: any) => any[]) => import QuickStatsCards from '../QuickStatsCards'; describe('QuickStatsCards', () => { - beforeEach(() => jest.useFakeTimers()); - afterEach(() => jest.useRealTimers()); - - it('renders stat labels for stars, streak, and levels', async () => { + it('renders a single compact text line with stars and level progress', async () => { const { UNSAFE_getAllByType } = render(); await act(async () => {}); - act(() => { - jest.advanceTimersByTime(700); - }); const texts = getAllTexts(UNSAFE_getAllByType); - expect(texts).toContain('home.stats.stars'); - expect(texts).toContain('home.stats.streak'); - expect(texts).toContain('home.stats.levels'); + expect(texts).toHaveLength(1); + // Total stars: 4 + 3 = 7; levels completed = 2 of 10 + expect(texts[0]).toBe('7 home.stats.stars · 2 home.stats.levelOf home.stats.levels'); }); - it('displays correct values after data load', async () => { + it('no longer shows the streak (lives in the mascot greeting only)', async () => { const { UNSAFE_getAllByType } = render(); await act(async () => {}); - act(() => { - jest.advanceTimersByTime(700); - }); - const texts = getAllTexts(UNSAFE_getAllByType); - // Total stars: 4 + 3 = 7 - expect(texts).toContain('7'); - // Current streak = 5 - expect(texts).toContain('5'); - // Levels completed = 2 - expect(texts).toContain('2'); - }); - - it('shows streak days label when streak > 0', async () => { - const { UNSAFE_getAllByType } = render(); - await act(async () => {}); - act(() => { - jest.advanceTimersByTime(700); - }); - const texts = getAllTexts(UNSAFE_getAllByType); - expect(texts).toContain('home.stats.streakDays'); - }); - - it('shows level-of sub label for the levels card', async () => { - const { UNSAFE_getAllByType } = render(); - await act(async () => {}); - act(() => { - jest.advanceTimersByTime(700); - }); - const texts = getAllTexts(UNSAFE_getAllByType); - // t('home.stats.levelOf', { total: '10' }) → key unchanged since key has no {{total}} placeholder - expect(texts).toContain('home.stats.levelOf'); + const texts = getAllTexts(UNSAFE_getAllByType).join(' '); + expect(texts).not.toContain('streak'); }); }); diff --git a/components/game/MemorizePhase.tsx b/components/game/MemorizePhase.tsx index 0c26090..b2f8d98 100644 --- a/components/game/MemorizePhase.tsx +++ b/components/game/MemorizePhase.tsx @@ -7,7 +7,7 @@ import Colors from '../../constants/Colors'; import type { MemorizePhaseProps } from './game.shared'; import { useTranslation } from '@services/i18n'; import { useTheme } from '@services/ThemeContext'; -import TimerArc from './TimerArc'; +import TimerBar from './TimerBar'; export default function MemorizePhase({ timeRemaining, @@ -29,7 +29,7 @@ export default function MemorizePhase({ {t('game.memorize.title')} - + {currentImage && ( diff --git a/components/game/TimerArc.native.tsx b/components/game/TimerArc.native.tsx deleted file mode 100644 index 4ac0aba..0000000 --- a/components/game/TimerArc.native.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { View, Text, StyleSheet } from 'react-native'; -import { Canvas, Path, Skia } from '@shopify/react-native-skia'; -import { Easing, useSharedValue, useDerivedValue, withTiming } from 'react-native-reanimated'; -import { useReduceMotion } from '../../utils/useReduceMotion'; -import Colors from '../../constants/Colors'; -import { FontSize, FontWeight } from '../../constants/Layout'; -import { getTimerArcColor } from './TimerArc.shared'; - -const SIZE = 80; -const STROKE_WIDTH = 6; -const PADDING = STROKE_WIDTH / 2; -const OVAL = { x: PADDING, y: PADDING, width: SIZE - STROKE_WIDTH, height: SIZE - STROKE_WIDTH }; - -export interface TimerArcProps { - timeRemaining: number; - totalTime: number; -} - -export function TimerArc({ timeRemaining, totalTime }: TimerArcProps) { - const reduceMotion = useReduceMotion(); - const progress = useSharedValue(totalTime > 0 ? timeRemaining / totalTime : 1); - - useEffect(() => { - const target = totalTime > 0 ? Math.max(0, timeRemaining) / totalTime : 0; - if (reduceMotion) { - progress.value = target; - } else { - progress.value = withTiming(target, { duration: 900, easing: Easing.linear }); - } - }, [timeRemaining, totalTime, reduceMotion, progress]); - - const trackPath = useMemo(() => { - const p = Skia.Path.Make(); - p.addArc(OVAL, -90, 360); - return p; - }, []); - - const arcPath = useDerivedValue(() => { - const p = Skia.Path.Make(); - const sweep = progress.value * 360; - if (sweep > 0.5) { - p.addArc(OVAL, -90, sweep); - } - return p; - }); - - const isUrgent = timeRemaining <= 3; - const arcColor = getTimerArcColor(timeRemaining); - - return ( - - - - - - - - {Math.max(0, Math.ceil(timeRemaining))} - - - - ); -} - -export default TimerArc; - -const styles = StyleSheet.create({ - container: { - width: SIZE, - height: SIZE, - alignSelf: 'center', - backgroundColor: Colors.primary, - borderRadius: SIZE / 2, - justifyContent: 'center', - alignItems: 'center', - ...Colors.shadow.large, - }, - canvas: { - position: 'absolute', - width: SIZE, - height: SIZE, - }, - timerText: { - fontSize: FontSize.huge, - fontWeight: FontWeight.bold, - color: '#ffffff', - textAlign: 'center', - lineHeight: SIZE, - }, - timerTextUrgent: { - color: Colors.timerArc.warning, - }, -}); diff --git a/components/game/TimerArc.shared.ts b/components/game/TimerArc.shared.ts deleted file mode 100644 index dcb74ec..0000000 --- a/components/game/TimerArc.shared.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Colors from '../../constants/Colors'; - -/** - * Shared urgency-color logic for TimerArc.native.tsx and TimerArc.web.tsx - * (previously duplicated verbatim in both files, Issue #334). - */ -export function getTimerArcColor(timeRemaining: number): string { - if (timeRemaining <= 1) return Colors.timerArc.critical; - if (timeRemaining <= 3) return Colors.timerArc.warning; - return Colors.timerArc.normal; -} diff --git a/components/game/TimerArc.tsx b/components/game/TimerArc.tsx deleted file mode 100644 index 80db04d..0000000 --- a/components/game/TimerArc.tsx +++ /dev/null @@ -1,3 +0,0 @@ -// Platform dispatcher — Metro picks .native.tsx, webpack picks .web.tsx. -export { TimerArc as default, TimerArc } from './TimerArc.native'; -export type { TimerArcProps } from './TimerArc.native'; diff --git a/components/game/TimerArc.web.tsx b/components/game/TimerArc.web.tsx deleted file mode 100644 index 0769a0c..0000000 --- a/components/game/TimerArc.web.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet } from 'react-native'; -import Svg, { Circle } from 'react-native-svg'; -import Colors from '../../constants/Colors'; -import { FontSize, FontWeight } from '../../constants/Layout'; -import { getTimerArcColor } from './TimerArc.shared'; - -const SIZE = 80; -const STROKE_WIDTH = 6; -const RADIUS = (SIZE - STROKE_WIDTH) / 2; -const CIRCUMFERENCE = 2 * Math.PI * RADIUS; -const CENTER = SIZE / 2; - -export interface TimerArcProps { - timeRemaining: number; - totalTime: number; -} - -export function TimerArc({ timeRemaining, totalTime }: TimerArcProps) { - const progress = totalTime > 0 ? Math.max(0, timeRemaining) / totalTime : 0; - const strokeDashoffset = CIRCUMFERENCE * (1 - progress); - const isUrgent = timeRemaining <= 3; - const arcColor = getTimerArcColor(timeRemaining); - - return ( - - - - - - - {Math.max(0, Math.ceil(timeRemaining))} - - - ); -} - -export default TimerArc; - -const styles = StyleSheet.create({ - container: { - width: SIZE, - height: SIZE, - alignSelf: 'center', - backgroundColor: Colors.primary, - borderRadius: SIZE / 2, - justifyContent: 'center', - alignItems: 'center', - ...Colors.shadow.large, - }, - timerText: { - fontSize: FontSize.huge, - fontWeight: FontWeight.bold, - color: '#ffffff', - textAlign: 'center', - }, - timerTextUrgent: { - color: Colors.timerArc.warning, - }, -}); diff --git a/components/game/TimerBar.tsx b/components/game/TimerBar.tsx new file mode 100644 index 0000000..46a5a54 --- /dev/null +++ b/components/game/TimerBar.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useRef } from 'react'; +import { View, Animated, Easing, StyleSheet } from 'react-native'; +import { useTheme } from '@services/ThemeContext'; +import { useReduceMotion } from '@utils/useReduceMotion'; +import { BorderRadius } from '../../constants/Layout'; + +export interface TimerBarProps { + timeRemaining: number; + totalTime: number; +} + +/** + * Dezente Restzeit-Anzeige der Merke-Phase: ein dünner Balken in einer Farbe, + * der leise abläuft. Ersetzt den früheren 80px-Countdown-Kreis mit großer Zahl + * und Gelb/Rot-Warnfarben — bei 3 s Anzeigezeit war der immer im "Alarm"-Modus. + */ +export default function TimerBar({ timeRemaining, totalTime }: TimerBarProps) { + const { colors } = useTheme(); + const reduceMotion = useReduceMotion(); + const target = totalTime > 0 ? Math.min(1, Math.max(0, timeRemaining) / totalTime) : 0; + const progress = useRef(new Animated.Value(target)).current; + + useEffect(() => { + if (reduceMotion) { + progress.setValue(target); + return; + } + const animation = Animated.timing(progress, { + toValue: target, + duration: 900, + easing: Easing.linear, + useNativeDriver: false, + }); + animation.start(); + return () => animation.stop(); + }, [target, reduceMotion, progress]); + + const width = progress.interpolate({ inputRange: [0, 1], outputRange: ['0%', '100%'] }); + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + track: { + alignSelf: 'center', + width: '60%', + maxWidth: 280, + height: 4, + borderRadius: BorderRadius.round, + overflow: 'hidden', + }, + fill: { + height: '100%', + borderRadius: BorderRadius.round, + }, +}); diff --git a/constants/Colors.ts b/constants/Colors.ts index 107d8b0..f067ff3 100644 --- a/constants/Colors.ts +++ b/constants/Colors.ts @@ -6,14 +6,14 @@ export const Colors = { // Primärfarben - Gradient-ready - // Blau-Familie passend zur App-Icon-/Store-Markenfarbe #60D5FA (Issue #306). - // #60D5FA selbst ist zu hell für Text/Buttons (Kontrast <2:1 gegen Weiß) — + // Blau-Familie passend zur App-Icon-/Store-Markenfarbe #A9DCEB (Issue #306; entsättigt, weniger aufdringlich). + // #A9DCEB selbst ist zu hell für Text/Buttons (Kontrast <2:1 gegen Weiß) — // primary/primaryDark sind daher dieselbe Farbfamilie in einem Ton, der // WCAG-Kontrast für weißen Button-Text einhält (~4:1, analog zum bisherigen // Violett-Wert); primaryLight bleibt die reine Icon-Farbe für dekorative // Flächen ohne Text (Kreise, Icon-Referenz, Splash-Hintergrund). primary: '#0585AD', - primaryLight: '#60D5FA', + primaryLight: '#A9DCEB', primaryDark: '#045B77', secondary: '#F093FB', // Rosa - Spielerisch & Warm accent: '#4ECDC4', // Mint-Teal - frischer Akzent @@ -115,15 +115,6 @@ export const Colors = { 5: '#E74C3C', // Rot - Sehr schwierig }, - // Timer-Zustandsfarben (TimerArc.native.tsx / TimerArc.web.tsx) — referenzieren - // bewusst dieselben Hex-Werte wie gradient.warm[1] / stars.filled, damit eine - // künftige Palettenänderung nur an einer Stelle nachgezogen werden muss (Issue #334). - timerArc: { - critical: '#FF6B6B', - warning: '#FFD700', - normal: '#FFFFFF', - }, - // UI-Akzentfarben außerhalb des Zeichnungskontexts (Home-Screen), zentralisiert // statt als Hex-Literal an der Verwendungsstelle (Issue #334). accents: { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7b5ef26..3010e25 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -33,7 +33,7 @@ Imports use the path aliases `@services/*`, `@components/*`, `@utils/*` │ ├── levels.tsx, gallery.tsx, creative.tsx, settings.tsx │ └── +html.tsx # web-only document head (SEO, meta) ├── components/ -│ ├── game/ # MemorizePhase, DrawPhase, ResultPhase, TimerArc +│ ├── game/ # MemorizePhase, DrawPhase, ResultPhase, TimerBar │ ├── DrawingCanvas.*.tsx # Skia canvas, split by platform │ └── … # Badge, Toast, Modal, Mascot components ├── services/ # Stateful managers, one domain each @@ -67,7 +67,7 @@ so timing stays in one place. ### Skia for drawing, split per platform `@shopify/react-native-skia` renders the canvas. `DrawingCanvas` and -`ConfettiBurst`/`TimerArc` exist as `.native.tsx` / `.web.tsx` pairs with a +`ConfettiBurst` exist as `.native.tsx` / `.web.tsx` pairs with a shared `.shared.ts` module, because the web and native Skia backends differ enough that runtime branching would be messier than separate files. diff --git a/locales/de/translations.json b/locales/de/translations.json index e60a25e..7c5b019 100644 --- a/locales/de/translations.json +++ b/locales/de/translations.json @@ -11,13 +11,9 @@ "galleryButton": "Meine Galerie", "creativeButton": "Freies Malen", "settingsButton": "Einstellungen", - "streakBadge": "🔥 {{count}}", "stats": { "stars": "Sterne", - "streak": "Serie", "levels": "Level", - "streakDay": "Tag", - "streakDays": "Tage", "levelOf": "von {{total}}" } }, @@ -187,7 +183,6 @@ "subtitle": "Bild des Tages", "start": "Challenge starten", "completed": "Heute erledigt ✓", - "countdown": "Neu in {{hours}}h {{minutes}}m", "level": "Level {{number}}" }, "errors": { @@ -293,7 +288,6 @@ "streak": "Schön, dass du wieder da bist! 🔥 {{count}} Tage in Folge!", "streakLong": "Wow, {{count}} Tage in Folge! Du bist unglaublich!" }, - "nextUnlock": "Noch {{count}} Sterne bis: {{item}}", "accessory": { "hat": "Hut", "glasses": "Sonnenbrille", diff --git a/locales/en/translations.json b/locales/en/translations.json index 791b0d7..2e0cf44 100644 --- a/locales/en/translations.json +++ b/locales/en/translations.json @@ -11,13 +11,9 @@ "galleryButton": "My Gallery", "creativeButton": "Creative", "settingsButton": "Settings", - "streakBadge": "🔥 {{count}}", "stats": { "stars": "Stars", - "streak": "Streak", "levels": "Levels", - "streakDay": "day", - "streakDays": "days", "levelOf": "of {{total}}" } }, @@ -187,7 +183,6 @@ "subtitle": "Picture of the Day", "start": "Start Challenge", "completed": "Done for today ✓", - "countdown": "New in {{hours}}h {{minutes}}m", "level": "Level {{number}}" }, "errors": { @@ -293,7 +288,6 @@ "streak": "Great to see you again! 🔥 {{count}} days in a row!", "streakLong": "Wow, {{count}} days in a row! You're amazing!" }, - "nextUnlock": "{{count}} more stars until: {{item}}", "accessory": { "hat": "Hat", "glasses": "Sunglasses", diff --git a/locales/es/translations.json b/locales/es/translations.json index c59df33..3cfd009 100644 --- a/locales/es/translations.json +++ b/locales/es/translations.json @@ -11,13 +11,9 @@ "galleryButton": "Mi galería", "creativeButton": "Creativo", "settingsButton": "Ajustes", - "streakBadge": "🔥 {{count}}", "stats": { "stars": "Estrellas", - "streak": "Racha", "levels": "Niveles", - "streakDay": "día", - "streakDays": "días", "levelOf": "de {{total}}" } }, @@ -187,7 +183,6 @@ "subtitle": "Imagen del día", "start": "Empezar reto", "completed": "Completado hoy ✓", - "countdown": "Nuevo en {{hours}}h {{minutes}}m", "level": "Nivel {{number}}" }, "errors": { @@ -293,7 +288,6 @@ "streak": "¡Qué bien que vuelvas! 🔥 ¡{{count}} días seguidos!", "streakLong": "¡Vaya, {{count}} días seguidos! ¡Eres increíble!" }, - "nextUnlock": "Faltan {{count}} estrellas para: {{item}}", "accessory": { "hat": "Sombrero", "glasses": "Gafas de sol", diff --git a/locales/fr/translations.json b/locales/fr/translations.json index bb02228..c259dc4 100644 --- a/locales/fr/translations.json +++ b/locales/fr/translations.json @@ -11,13 +11,9 @@ "galleryButton": "Ma galerie", "creativeButton": "Créatif", "settingsButton": "Réglages", - "streakBadge": "🔥 {{count}}", "stats": { "stars": "Étoiles", - "streak": "Série", "levels": "Niveaux", - "streakDay": "jour", - "streakDays": "jours", "levelOf": "sur {{total}}" } }, @@ -187,7 +183,6 @@ "subtitle": "Image du jour", "start": "Commencer le défi", "completed": "Terminé pour aujourd'hui ✓", - "countdown": "Nouveau dans {{hours}}h {{minutes}}m", "level": "Niveau {{number}}" }, "errors": { @@ -293,7 +288,6 @@ "streak": "Content de te revoir ! 🔥 {{count}} jours de suite !", "streakLong": "Waouh, {{count}} jours de suite ! Tu es incroyable !" }, - "nextUnlock": "Encore {{count}} étoiles avant : {{item}}", "accessory": { "hat": "Chapeau", "glasses": "Lunettes de soleil", diff --git a/locales/it/translations.json b/locales/it/translations.json index 1a1d62d..5bf7614 100644 --- a/locales/it/translations.json +++ b/locales/it/translations.json @@ -11,13 +11,9 @@ "galleryButton": "La mia galleria", "creativeButton": "Creativo", "settingsButton": "Impostazioni", - "streakBadge": "🔥 {{count}}", "stats": { "stars": "Stelle", - "streak": "Serie", "levels": "Livelli", - "streakDay": "giorno", - "streakDays": "giorni", "levelOf": "di {{total}}" } }, @@ -187,7 +183,6 @@ "subtitle": "Immagine del giorno", "start": "Inizia sfida", "completed": "Completata per oggi ✓", - "countdown": "Nuova tra {{hours}}h {{minutes}}m", "level": "Livello {{number}}" }, "errors": { @@ -293,7 +288,6 @@ "streak": "Che bello rivederti! 🔥 {{count}} giorni di fila!", "streakLong": "Wow, {{count}} giorni di fila! Sei incredibile!" }, - "nextUnlock": "Ancora {{count}} stelle per: {{item}}", "accessory": { "hat": "Cappello", "glasses": "Occhiali da sole", diff --git a/locales/nl/translations.json b/locales/nl/translations.json index 2b19abd..8a11e89 100644 --- a/locales/nl/translations.json +++ b/locales/nl/translations.json @@ -11,13 +11,9 @@ "galleryButton": "Mijn galerij", "creativeButton": "Creatief", "settingsButton": "Instellingen", - "streakBadge": "🔥 {{count}}", "stats": { "stars": "Sterren", - "streak": "Reeks", "levels": "Levels", - "streakDay": "dag", - "streakDays": "dagen", "levelOf": "van {{total}}" } }, @@ -187,7 +183,6 @@ "subtitle": "Afbeelding van de dag", "start": "Uitdaging starten", "completed": "Vandaag voltooid ✓", - "countdown": "Nieuw over {{hours}}u {{minutes}}m", "level": "Level {{number}}" }, "errors": { @@ -293,7 +288,6 @@ "streak": "Leuk dat je er weer bent! 🔥 {{count}} dagen op rij!", "streakLong": "Wow, {{count}} dagen op rij! Je bent geweldig!" }, - "nextUnlock": "Nog {{count}} sterren tot: {{item}}", "accessory": { "hat": "Hoed", "glasses": "Zonnebril", diff --git a/locales/pl/translations.json b/locales/pl/translations.json index c1fc6d5..e132345 100644 --- a/locales/pl/translations.json +++ b/locales/pl/translations.json @@ -11,13 +11,9 @@ "galleryButton": "Moja galeria", "creativeButton": "Kreatywny", "settingsButton": "Ustawienia", - "streakBadge": "🔥 {{count}}", "stats": { "stars": "Gwiazdki", - "streak": "Seria", "levels": "Poziomy", - "streakDay": "dzień", - "streakDays": "dni", "levelOf": "z {{total}}" } }, @@ -187,7 +183,6 @@ "subtitle": "Obrazek dnia", "start": "Rozpocznij wyzwanie", "completed": "Ukończono na dziś ✓", - "countdown": "Nowe za {{hours}}h {{minutes}}m", "level": "Poziom {{number}}" }, "errors": { @@ -293,7 +288,6 @@ "streak": "Miło Cię znów widzieć! 🔥 {{count}} dni z rzędu!", "streakLong": "Wow, {{count}} dni z rzędu! Jesteś niesamowity!" }, - "nextUnlock": "Jeszcze {{count}} gwiazdek do: {{item}}", "accessory": { "hat": "Kapelusz", "glasses": "Okulary przeciwsłoneczne", diff --git a/package-lock.json b/package-lock.json index a128d5d..6d8db81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1525,7 +1525,7 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { @@ -3016,7 +3016,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -3034,7 +3034,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -3094,7 +3094,7 @@ "version": "30.4.0", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3119,7 +3119,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "expect": "^29.7.0", @@ -3133,7 +3133,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" @@ -3163,7 +3163,7 @@ "version": "30.1.0", "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3173,7 +3173,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -3189,7 +3189,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -3233,7 +3233,7 @@ "version": "1.1.21", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -3245,7 +3245,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -3266,7 +3266,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", @@ -3283,7 +3283,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -3296,7 +3296,7 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3321,7 +3321,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", @@ -3336,7 +3336,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -3352,7 +3352,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -4761,7 +4761,7 @@ "version": "13.3.3", "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.3.3.tgz", "integrity": "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "jest-matcher-utils": "^30.0.5", @@ -4788,7 +4788,7 @@ "version": "30.4.1", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" @@ -4801,14 +4801,14 @@ "version": "0.34.52", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@testing-library/react-native/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -4821,7 +4821,7 @@ "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.4.0", @@ -4837,7 +4837,7 @@ "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", @@ -4853,7 +4853,7 @@ "version": "30.4.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/schemas": "30.4.1", @@ -4992,7 +4992,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -5942,7 +5942,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -6006,7 +6006,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -6063,7 +6063,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cli-cursor": { @@ -6119,7 +6119,7 @@ }, "node_modules/co": { "version": "4.6.0", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "iojs": ">= 1.0.0", @@ -6128,7 +6128,7 @@ }, "node_modules/collect-v8-coverage": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/color": { @@ -6201,14 +6201,15 @@ } }, "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", + "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", + "destroy": "1.2.0", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", @@ -6216,6 +6217,10 @@ }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/compression/node_modules/debug": { @@ -6301,7 +6306,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -6423,7 +6428,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-urls": { @@ -6478,7 +6483,7 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -6596,7 +6601,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -6612,7 +6617,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -6769,7 +6774,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -6810,7 +6815,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -7231,7 +7236,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -7255,7 +7260,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">= 0.8.0" } @@ -7264,7 +7269,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", @@ -7452,7 +7457,6 @@ "version": "55.0.17", "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-55.0.17.tgz", "integrity": "sha512-rc41rUbFYdJ4IdPxWQiTDtkOlEbtYQkvsCASzW1pqskfRShAd4tYyyAw1BTez3Cq1IgWkK3wcGR+HbFdzZ7ImQ==", - "dev": true, "license": "MIT", "dependencies": { "expo-constants": "~55.0.17", @@ -8007,6 +8011,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8095,7 +8113,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -8226,13 +8244,13 @@ "license": "MIT" }, "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.5", + "minimist": "^1.2.8", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" @@ -8368,7 +8386,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/http-errors": { @@ -8432,7 +8450,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -8513,7 +8531,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -8542,7 +8560,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -8587,7 +8605,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/is-core-module": { @@ -8643,7 +8661,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -8701,7 +8719,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -8764,7 +8782,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -8779,7 +8797,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", @@ -8794,7 +8812,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -8808,7 +8826,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -8835,7 +8853,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "execa": "^5.0.0", @@ -8850,7 +8868,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -8882,7 +8900,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -8916,7 +8934,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -8962,7 +8980,7 @@ "version": "1.1.21", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -8974,7 +8992,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -8995,7 +9013,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9008,7 +9026,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -9024,7 +9042,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" @@ -9037,7 +9055,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -9169,7 +9187,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", @@ -9183,7 +9201,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -9233,7 +9251,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -9260,7 +9278,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -9281,7 +9299,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", @@ -9295,7 +9313,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -9328,7 +9346,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -9362,7 +9380,7 @@ "version": "1.1.21", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9374,7 +9392,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -9395,7 +9413,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9408,7 +9426,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -9440,7 +9458,7 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9629,7 +9647,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -9775,7 +9793,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -10196,7 +10214,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/locate-path": { @@ -10382,7 +10400,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -10398,7 +10416,7 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10906,7 +10924,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -10916,7 +10934,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -11004,7 +11022,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/negotiator": { @@ -11129,7 +11147,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -11228,7 +11246,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -11387,7 +11405,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -11452,7 +11470,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -11662,7 +11680,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -11947,7 +11965,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -12075,7 +12093,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/react-is-19": { @@ -12083,7 +12101,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/react-native": { @@ -12488,7 +12506,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.2.0.tgz", "integrity": "sha512-zLCFMHFE9vy/w3AxO0zNxy6aAupnCuLSVOJYDe/Tp+ayGI1f2PLQsFVPANSD42gdSbmYx5oN+1VWDhcXtq7hAQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "react-is": "^19.2.0", @@ -12502,7 +12520,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "indent-string": "^4.0.0", @@ -12619,7 +12637,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -12647,7 +12665,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -13026,9 +13044,9 @@ } }, "node_modules/shell-quote": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", - "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -13103,9 +13121,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -13115,7 +13133,7 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -13268,7 +13286,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "char-regex": "^1.0.2", @@ -13308,7 +13326,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -13318,7 +13336,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -13328,7 +13346,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "min-indent": "^1.0.0" @@ -13341,7 +13359,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -13735,7 +13753,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13985,7 +14003,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -14341,7 +14359,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 932635a..06f8e26 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/feature-graphic.png b/public/feature-graphic.png index 53bb077..8fd1455 100644 Binary files a/public/feature-graphic.png and b/public/feature-graphic.png differ diff --git a/scripts/generate-icon-assets.js b/scripts/generate-icon-assets.js index b98e340..7b26d09 100644 --- a/scripts/generate-icon-assets.js +++ b/scripts/generate-icon-assets.js @@ -37,7 +37,22 @@ const TARGETS = [ { svg: 'splash-icon.svg', out: 'splash-icon.png', width: 1024, height: 1024, transparent: true }, { svg: 'app-icon.svg', out: 'favicon.png', width: 1024, height: 1024 }, { svg: 'app-icon.svg', out: 'app-icon-512.png', width: 512, height: 512 }, + // Web/PWA-Icon (app.json web.icons + apple-touch-icon in post-build.js) — + // lag außerhalb von assets/icons/ und zeigte deshalb lange noch das alte Motiv. + { + svg: 'app-icon.svg', + out: path.join('..', '..', 'public', 'apple-touch-icon.png'), + width: 512, + height: 512, + }, { svg: 'feature-graphic.svg', out: 'feature-graphic.png', width: 1024, height: 500 }, + // og:image/twitter:image der Web-Demo (post-build.js) + { + svg: 'feature-graphic.svg', + out: path.join('..', '..', 'public', 'feature-graphic.png'), + width: 1024, + height: 500, + }, ]; async function main() { diff --git a/services/ThemeContext.tsx b/services/ThemeContext.tsx index eec1f9d..72080df 100644 --- a/services/ThemeContext.tsx +++ b/services/ThemeContext.tsx @@ -74,11 +74,11 @@ const ThemeContext = createContext(undefined); // Light theme colors // primary ist eine kontraststarke Blau-Variante (WCAG-safe für Text/Buttons -// auf hellem Grund), primaryLight die eigentliche Icon-/Markenfarbe #60D5FA +// auf hellem Grund), primaryLight die eigentliche Icon-/Markenfarbe #A9DCEB // für dekorative Flächen ohne Text (Issue #306). const LIGHT_COLORS: ThemeColors = { primary: '#0585AD', - primaryLight: '#60D5FA', + primaryLight: '#A9DCEB', primaryDark: '#045B77', secondary: '#F093FB', accent: '#4ECDC4',