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.
This commit is contained in:
Bruno Deanoz 2026-06-21 15:04:31 +02:00
parent 0237eae1e6
commit e3d624c961
2 changed files with 21 additions and 62 deletions

View file

@ -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 2630 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 24 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

View file

@ -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