chahinebrini 5d6c322129 wip: KeyboardAwareSheet migrations + Snake/Tetris UI + iron.png + useMe live-update
Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow
mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail.
GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift.

- KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding
- react-native-keyboard-controller installiert + KeyboardProvider in Root
- Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO)
- Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn
- DPad-Buttons 60→48, more bg, no scale
- useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates
- dm.tsx: resolveAvatar wrap (iron.png-Warning)
- Mail-error-humanizer + locales

Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:59:25 +02:00

58 lines
1.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<span ref="spanEl">{{ display }}</span>
</template>
<script setup lang="ts">
/**
* AnimatedCounter zählt von 0 auf einen Zielwert hoch.
* Verwendet requestAnimationFrame mit Ease-out-Cubic.
*
* Props:
* target Zielwert (required)
* duration Animationsdauer in ms (default 1800)
* delay Startverzögerung in ms (default 0)
* decimals Nachkommastellen (default 0)
*/
const props = withDefaults(defineProps<{
target: number;
duration?: number;
delay?: number;
decimals?: number;
}>(), {
duration: 1800,
delay: 0,
decimals: 0,
});
const display = ref(props.decimals > 0 ? (0).toFixed(props.decimals) : '0');
const spanEl = ref<HTMLElement | null>(null);
let started = false;
function runAnimation() {
const startTime = performance.now();
const update = (now: number) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / props.duration, 1);
const eased = 1 - Math.pow(1 - progress, 3); // ease-out cubic
const current = eased * props.target;
display.value = props.decimals > 0
? current.toFixed(props.decimals)
: Math.round(current).toString();
if (progress < 1) requestAnimationFrame(update);
};
requestAnimationFrame(update);
}
onMounted(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !started) {
started = true;
observer.disconnect();
setTimeout(runAnimation, props.delay);
}
}, { threshold: 0.3 });
if (spanEl.value) observer.observe(spanEl.value);
onUnmounted(() => observer.disconnect());
});
</script>