From e3d624c961615006d6f48d3c825663ba1295f784 Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Sun, 21 Jun 2026 15:04:31 +0200 Subject: [PATCH] feat: orb uses rememberTiltState(), update CLAUDE.md with i18n + migration status KaizenOrb now uses centralized rememberTiltState() (GAME_ROTATION_VECTOR, lifecycle-aware, battery-saver-aware) instead of inline sensor code. Breath animation uses Durations tokens from motion system. CLAUDE.md: updated design system section with GlassSurface correction (no backdrop-filter on Android), migration status (all components done), i18n section documenting the de/en string resource architecture. --- CLAUDE.md | 18 ++++- .../java/dev/kaizen/app/chat/Background.kt | 65 ++----------------- 2 files changed, 21 insertions(+), 62 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c13eeed..c274c41 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,7 +48,7 @@ ui/ theme/ Color (P3), Theme (multi-theme), Type, Oklab, Dither theme/themes/ AccentScheme, AmberTheme, BlauTheme, MonoTheme, AuroraTheme, ThemeRegistry shape/ SquircleShape (superellipse), KaizenShapes (token system) - effect/ GlassSurface (RenderEffect blur), Shadow (multi-layer stack) + effect/ GlassSurface (frosted glass), Shadow (multi-layer stack) motion/ Motion (easing curves, spring specs, duration tokens) sensor/ TiltState (gyro/accel parallax, lifecycle-aware) ``` @@ -69,14 +69,26 @@ Full spec: `DESIGN.md`. Branch: `feature/design-system-v2`. 1. **P3 color pipeline** — all tokens in DisplayP3 color space (`Color.kt`). `window.colorMode = COLOR_MODE_HDR` (API 34+) / `COLOR_MODE_WIDE_COLOR_GAMUT` (API 26+) in `MainActivity`. No `Color(0xFF…)` for brand colors outside `Color.kt`. 2. **Squircle shapes** — iOS-style superellipse with G2 continuity (`ui/shape/Squircle.kt`). Token system `KaizenShapes.{xs,sm,md,lg,xl,pill,circle}`. Replaces all `RoundedCornerShape`. -3. **GlassSurface** — real `Modifier.blur()` (RenderEffect on API 31+), tint + inner highlight + border. API 26–30 fallback: higher tint alpha. Opacity tiers per component in `GlassTiers`. +3. **GlassSurface** — frosted-glass composable (gradient tint + specular border + inner highlight + multi-layer shadow). Android has no backdrop-filter; glass effect works via semi-transparent tint over the blob layer. Opacity tiers per component in `GlassTiers`. 4. **Multi-layer shadows** — `ShadowStack` with 2–4 overlapping layers (`KaizenShadows.level0..4`). Dark mode multiplies alphas ×1.8. Replaces all `Modifier.shadow()`. 5. **4-theme system** — Amber/Blau/Mono/Aurora. Each theme = `KaizenAccentScheme` (primary + 4 blob colors). Neutrals are theme-independent. `LocalKaizenAccent` CompositionLocal. Server sync planned via `GET /api/v1/me` → `uiTheme`. 6. **Sensor-reactive motion** — `rememberTiltState()` provides smoothed tilt `Offset(-1..1)`. Uses `TYPE_GAME_ROTATION_VECTOR` (fused, no drift). Auto-disables on battery saver, reduced motion, background, extreme tilt. **Motion tokens** (`ui/motion/Motion.kt`): `EaseSpring` (overshoot), `EaseSmooth` (expo-out), `EaseEmphasized` (M3), `SpringSnappy/Default/Gentle`. All durations centralised in `Durations` object. -**What's NOT yet migrated to the design system:** The existing UI components (ChatScreen, Sidebar, ChatInput, etc.) still use the old inline glass styling with manual `Brush` + `border`. Migration to `GlassSurface` + `KaizenShapes` + `kaizenShadow` is the next step. +**Migration status:** All UI components migrated to the design system. Sidebar, ChatInput, ChatScreen, LoginScreen, ModelSheet, SettingsCard, SettingsScreen use `GlassSurface` + `KaizenShapes` + `kaizenShadow`. Only Markdown code-block shapes remain as `RoundedCornerShape` (intentionally rectangular for code). + +**KaizenOrb** (`chat/Background.kt`) uses `rememberTiltState()` from `ui/sensor/` instead of inline sensor code. Breath animation uses `Durations.ORB_BREATH` / `ORB_BREATH_STREAMING` from `ui/motion/`. + +### Internationalization (i18n) + +- **de** (default), **en** — same as web (`messages/de.json`, `messages/en.json`). +- Strings: `res/values/strings.xml` (de) + `res/values-en/strings.xml` (en). +- All UI strings use `stringResource(R.string.xxx)` in `@Composable` functions, or `context.getString(R.string.xxx)` in non-composable contexts (error handlers, coroutines). +- Date/time: `DateTimeFormatter` with `Locale.getDefault()`. +- Greeting: time-of-day-based (Morgen/Tag/Abend → Morning/Afternoon/Evening), boundaries 5/12/18. +- ChatModels: `Suggestion.labelRes`/`promptRes` and `ChatMode.labelRes` are resource IDs (Int), not hardcoded strings. +- `ModePillsRow`: `selected` state is `Int` (resource ID), not `String`. ### Offline-first cache layer (Room) — added 2026-06-21 diff --git a/app/src/main/java/dev/kaizen/app/chat/Background.kt b/app/src/main/java/dev/kaizen/app/chat/Background.kt index 8e136df..42b29bc 100644 --- a/app/src/main/java/dev/kaizen/app/chat/Background.kt +++ b/app/src/main/java/dev/kaizen/app/chat/Background.kt @@ -1,23 +1,14 @@ package dev.kaizen.app.chat -import android.content.Context -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -27,11 +18,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.BlurredEdgeTreatment @@ -44,9 +31,10 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import dev.kaizen.app.ui.motion.Durations +import dev.kaizen.app.ui.sensor.rememberTiltState import dev.kaizen.app.ui.theme.Amber import dev.kaizen.app.ui.theme.AmberDeep import dev.kaizen.app.ui.theme.BlobAmber @@ -132,58 +120,17 @@ fun KaizenOrb( initialValue = 1f, targetValue = if (streaming || recording) 1.05f else 1.025f, animationSpec = infiniteRepeatable( - tween(if (streaming || recording) 2400 else 7000, easing = FastOutSlowInEasing), + tween(if (streaming || recording) Durations.ORB_BREATH_STREAMING else Durations.ORB_BREATH, easing = FastOutSlowInEasing), RepeatMode.Reverse, ), label = "breath", ) - // Dynamic scale expansion based on real-time audio amplitude (Call Mode / TTS speech reactivity) val reactiveScale = amplitude?.let { 1f + it * 0.12f } ?: 1f - // --- Gyroscope / Accel-based Holographic Parallax Offset --- - val context = LocalContext.current - var rawTiltX by remember { mutableStateOf(0f) } - var rawTiltY by remember { mutableStateOf(0f) } - - DisposableEffect(Unit) { - val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager - val gravitySensor = sensorManager?.getDefaultSensor(Sensor.TYPE_GRAVITY) - ?: sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) - - val listener = object : SensorEventListener { - override fun onSensorChanged(event: SensorEvent?) { - if (event == null) return - // event.values[0] is X (left/right tilt), event.values[1] is Y (forward/backward tilt) - // Normalize gravity acceleration to unit scale [-1.0, 1.0] - val x = event.values[0] / 9.81f - val y = event.values[1] / 9.81f - rawTiltX = x.coerceIn(-1f, 1f) - rawTiltY = y.coerceIn(-1f, 1f) - } - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} - } - - if (sensorManager != null && gravitySensor != null) { - sensorManager.registerListener(listener, gravitySensor, SensorManager.SENSOR_DELAY_UI) - } - - onDispose { - sensorManager?.unregisterListener(listener) - } - } - - // Smooth raw tilt values using native springs for fluid, liquid responsiveness - val tiltX by animateFloatAsState( - targetValue = rawTiltX, - animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow), - label = "tiltX" - ) - val tiltY by animateFloatAsState( - targetValue = rawTiltY, - animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow), - label = "tiltY" - ) + val tilt by rememberTiltState() + val tiltX = tilt.x + val tiltY = tilt.y Box( modifier = modifier