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:
parent
0237eae1e6
commit
e3d624c961
2 changed files with 21 additions and 62 deletions
18
CLAUDE.md
18
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue