Remove haze

This commit is contained in:
2026-05-10 12:01:54 +02:00
parent 568e793c44
commit 573b4562c2
27 changed files with 12 additions and 1384 deletions

View File

@@ -1,9 +1,5 @@
package dev.ulfrx.recipe.di
import com.russhwolf.settings.Settings
import dev.ulfrx.recipe.ui.components.glass.GlassBackend
import dev.ulfrx.recipe.ui.components.glass.isDebugBuild
import dev.ulfrx.recipe.ui.components.glass.resolveGlassBackend
import dev.ulfrx.recipe.ui.screens.pantry.PantrySearchViewModel
import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
@@ -14,37 +10,8 @@ import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel
import org.koin.dsl.module
import org.koin.plugin.module.dsl.viewModel
/**
* Phase 2.1 (UI-03 / UI-04 / UI-09 / UI-10) — DI module for the app-shell layer.
*
* Registers:
* - 4 tab ViewModels (Planner / Recipes / Pantry / Shopping) — pure StateFlow,
* no dependencies this phase. Phase 5+ extends each to inject repositories.
* - 2 Search ViewModels (Recipes + Pantry) — pure StateFlow with nullable
* `searchSource: SearchSource? = null` per RESEARCH § Pattern 4.
* - 1 ShellViewModel — active-tab + search-open state machine.
* - 1 GlassBackend single — resolved at composition root from
* [resolveGlassBackend] (CONTEXT D-16 / D-17). Default backend is
* [GlassBackend.Liquid] — the iOS+Android primary path; debug builds may
* pick up a runtime override stored in `multiplatform-settings`.
*
* Settings binding: registered in platform-specific Koin modules
* (`auth/IosAuthModule.kt`, `auth/AndroidAuthModule.kt`) for use by
* SecureAuthStateStore — the same single<Settings> binding is reused here.
*/
val shellModule =
module {
// Glass backend — resolved once at startup. Production builds short-circuit
// [resolveGlassBackend] via [isDebugBuild] = false; debug builds may pick up
// a runtime override stored in `multiplatform-settings`.
single<GlassBackend> {
resolveGlassBackend(
settings = get<Settings>(),
isDebug = isDebugBuild,
default = GlassBackend.Liquid,
)
}
// Shell-level state machine.
viewModel<ShellViewModel>()

View File

@@ -52,7 +52,7 @@ import recipe.composeapp.generated.resources.search_close_a11y
* [animateContentSize] (size) + [AnimatedContent] (content swap) at 250ms with
* [FastOutSlowInEasing] per UI-SPEC line 198.
*
* Substrate: [GlassSurface] from plan 02.1-03 — direct Liquid/Haze API calls are
* Substrate: [GlassSurface] from plan 02.1-03 — direct Liquid API calls are
* forbidden here per CLAUDE.md non-negotiable #10.
*
* Touch targets: each tab cell + collapsed toggle is ≥ 44dp (UI-SPEC line 52, 224).

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Search
@@ -30,10 +31,9 @@ import recipe.composeapp.generated.resources.search_open_a11y
*/
@Composable
fun FloatingSearchButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
) {
val a11y = stringResource(Res.string.search_open_a11y)
GlassSurface(
modifier = modifier.size(56.dp),
cornerRadius = 28.dp,
@@ -49,7 +49,7 @@ fun FloatingSearchButton(
) {
UnstyledIcon(
imageVector = Lucide.Search,
contentDescription = a11y,
contentDescription = stringResource(Res.string.search_open_a11y),
tint = RecipeTheme.colors.content,
modifier = Modifier.size(24.dp),
)
@@ -57,3 +57,4 @@ fun FloatingSearchButton(
}
}
}

View File

@@ -1,36 +0,0 @@
package dev.ulfrx.recipe.ui.components.glass
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
/**
* Flat translucent fallback with no blur. Geometry matches Liquid/Haze so
* chrome call sites never branch on the active backend.
*/
@Composable
internal fun FlatGlassSurface(
modifier: Modifier,
tint: Color,
cornerRadius: Dp,
border: BorderStroke?,
content: @Composable BoxScope.() -> Unit,
) {
val shape = RoundedCornerShape(cornerRadius)
Box(
modifier =
modifier
.clip(shape)
.background(tint, shape)
.let { if (border != null) it.border(border, shape) else it },
content = content,
)
}

View File

@@ -13,13 +13,12 @@ import androidx.compose.ui.Modifier
* Shared source/sampling state for glass chrome.
*
* AppShell wraps the screen body in [GlassBackdropSource]. GlassSurface backends
* consume [LocalGlassBackdropState] so Liquid/Haze sample the same layer behind
* consume [LocalGlassBackdropState] so Liquid sample the same layer behind
* the dock/search chrome.
*/
@Stable
class GlassBackdropState internal constructor(
internal val liquidState: Any,
internal val hazeState: Any,
)
val LocalGlassBackdropState = compositionLocalOf<GlassBackdropState?> { null }
@@ -27,11 +26,9 @@ val LocalGlassBackdropState = compositionLocalOf<GlassBackdropState?> { null }
@Composable
fun rememberGlassBackdropState(): GlassBackdropState {
val liquidState = rememberLiquidBackdropHandle()
val hazeState = rememberHazeBackdropHandle()
return remember(liquidState, hazeState) {
return remember(liquidState) {
GlassBackdropState(
liquidState = liquidState,
hazeState = hazeState,
liquidState = liquidState
)
}
}
@@ -46,8 +43,7 @@ fun GlassBackdropSource(
Box(
modifier =
modifier
.liquidBackdropSource(state)
.hazeBackdropSource(state),
.liquidBackdropSource(state),
content = content,
)
}

View File

@@ -1,46 +0,0 @@
package dev.ulfrx.recipe.ui.components.glass
import androidx.compose.runtime.compositionLocalOf
import com.russhwolf.settings.Settings
/**
* Three glass-effect backends per CONTEXT D-16. All three consume the same
* token API so chrome call sites never branch on the active backend.
*/
enum class GlassBackend {
Liquid,
Haze,
Flat,
}
/**
* Composition root sets this to the resolved backend for the running build.
* Consumers outside a provider fail safe to the simplest visible substrate.
*/
val LocalGlassBackend = compositionLocalOf { GlassBackend.Flat }
/**
* Debug-only runtime override key (D-17). Values: "liquid", "haze", "flat".
*/
const val DEBUG_GLASS_BACKEND_KEY: String = "debug.glass_backend"
/**
* Pure backend resolver used by production code and common tests.
*
* Release builds return [default] without consulting settings, so production
* binaries do not carry a runtime backend switch.
*/
fun resolveGlassBackend(
settings: Settings,
isDebug: Boolean,
default: GlassBackend,
): GlassBackend {
if (!isDebug) return default
val raw = settings.getStringOrNull(DEBUG_GLASS_BACKEND_KEY) ?: return default
return when (raw.lowercase()) {
"liquid" -> GlassBackend.Liquid
"haze" -> GlassBackend.Haze
"flat" -> GlassBackend.Flat
else -> default
}
}

View File

@@ -9,11 +9,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import dev.ulfrx.recipe.ui.theme.RecipeTheme
/**
* Single public entry point for glass-effect chrome. Dispatches to one backend
* through [LocalGlassBackend] and consumes the shared backdrop source when one
* is present above it.
*/
@Composable
fun GlassSurface(
modifier: Modifier = Modifier,
@@ -23,9 +18,5 @@ fun GlassSurface(
content: @Composable BoxScope.() -> Unit,
) {
val backdropState = LocalGlassBackdropState.current
when (LocalGlassBackend.current) {
GlassBackend.Liquid -> LiquidGlassSurface(modifier, tint, cornerRadius, border, backdropState, content)
GlassBackend.Haze -> HazeGlassSurface(modifier, tint, cornerRadius, border, backdropState, content)
GlassBackend.Flat -> FlatGlassSurface(modifier, tint, cornerRadius, border, content)
}
LiquidGlassSurface(modifier, tint, cornerRadius, border, backdropState, content)
}

View File

@@ -1,58 +0,0 @@
package dev.ulfrx.recipe.ui.components.glass
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.rememberHazeState
/**
* Haze 1.x backend per CONTEXT D-16. The actual 1.6.10 API takes a
* HazeStyle/block instead of a shape parameter, so shape is enforced by the
* surrounding clip while the effect consumes the shared [HazeState].
*/
@Composable
internal fun HazeGlassSurface(
modifier: Modifier,
tint: Color,
cornerRadius: Dp,
border: BorderStroke?,
backdropState: GlassBackdropState?,
content: @Composable BoxScope.() -> Unit,
) {
val state = backdropState?.hazeState as? HazeState ?: rememberHazeState()
val shape = RoundedCornerShape(cornerRadius)
val style =
HazeStyle(
backgroundColor = tint.copy(alpha = 1f),
tint = HazeTint(tint),
blurRadius = 24.dp,
)
Box(
modifier =
modifier
.clip(shape)
.hazeEffect(state, style)
.background(tint, shape)
.let { if (border != null) it.border(border, shape) else it },
content = content,
)
}
@Composable
internal fun rememberHazeBackdropHandle(): Any = rememberHazeState()
internal fun Modifier.hazeBackdropSource(state: GlassBackdropState): Modifier = hazeSource(state.hazeState as HazeState)

View File

@@ -1,7 +0,0 @@
package dev.ulfrx.recipe.ui.components.glass
/**
* Compile-time gate for the [resolveGlassBackend] runtime override path
* (CONTEXT D-17).
*/
expect val isDebugBuild: Boolean

View File

@@ -111,10 +111,6 @@ fun SearchPill(
}
}
/**
* Internal helper — placeholder text rendered when the BasicTextField is empty.
* Plain text in [RecipeTheme.typography.body] tinted [RecipeTheme.colors.contentMuted].
*/
@Composable
private fun PlaceholderText(
text: String,

View File

@@ -61,7 +61,7 @@ import recipe.composeapp.generated.resources.search_dismiss_keyboard_a11y
* Layout responsibilities:
* - Background: full-screen [RecipeTheme.colors.background] under the safe area.
* - Body: [RootNavHost] consumes the full screen, wrapped in [GlassBackdropSource]
* so Liquid/Haze chrome sample the screen body through [LocalGlassBackdropState].
* so Liquid chrome samples the screen body through [LocalGlassBackdropState].
* - Bottom chrome (overlay): bottom-anchored Column containing optional [SearchPill]
* (when ui.searchOpen && active.hasSearch) and the [DockBar] (always visible).
* Chrome consumes [WindowInsets.navigationBars] + [imePadding] explicitly per
@@ -137,7 +137,7 @@ fun AppShell(modifier: Modifier = Modifier) {
.background(RecipeTheme.colors.background),
) {
// Body — RootNavHost fills the available space and is the shared source layer
// for Liquid/Haze chrome sampling via GlassBackdropSource (plan 02.1-03).
// for Liquid chrome sampling via GlassBackdropSource (plan 02.1-03).
GlassBackdropSource(modifier = Modifier.fillMaxSize()) {
RootNavHost(
navController = navController,

View File

@@ -5,9 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.ReadOnlyComposable
import dev.ulfrx.recipe.ui.components.glass.GlassBackend
import dev.ulfrx.recipe.ui.components.glass.LocalGlassBackend
import org.koin.compose.koinInject
/**
* Recipe theme entry point (CONTEXT D-14, D-15).
@@ -35,7 +32,6 @@ public val LocalRecipeGlass: ProvidableCompositionLocal<RecipeGlass> =
public fun RecipeTheme(content: @Composable () -> Unit) {
val dark = isSystemInDarkTheme()
val recipeColors = if (dark) DarkRecipeColors else LightRecipeColors
val glassBackend = koinInject<GlassBackend>()
CompositionLocalProvider(
LocalRecipeColors provides recipeColors,
@@ -43,7 +39,6 @@ public fun RecipeTheme(content: @Composable () -> Unit) {
LocalRecipeSpacing provides DefaultRecipeSpacing,
LocalRecipeShapes provides DefaultRecipeShapes,
LocalRecipeGlass provides DefaultRecipeGlass,
LocalGlassBackend provides glassBackend,
content = content,
)
}