3 Commits

Author SHA1 Message Date
9bbef911c0 fix(components): match full-screen picker bar icons to the runtime theme
A Compose Dialog owns its own Window whose status/navigation-bar icon
appearance is seeded from the XML theme, not from the host activity's
runtime edge-to-edge state. With decorFitsSystemWindows = false the
FullScreenPicker draws under the bars, exposing icons that don't match
the active theme — dark icons on a dark picker (near-invisible), and the
mirror-image nav-bar bug in light theme.

Expose the resolved light/dark decision from FloretExpressiveTheme as
LocalFloretDarkTheme and drive the dialog window's
isAppearanceLight{Status,Navigation}Bars from it, so the appearance
tracks the in-app light/dark choice even when it diverges from the
system night resource qualifier.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:11:16 +02:00
75b90d1b3b Merge pull request 'feat(components): expose SelectedCheck as a family primitive' (#2) from feat/selected-check-primitive into main 2026-07-20 13:27:22 +00:00
b4954713ff feat(components): expose SelectedCheck as a family primitive
The trailing check that marks a chosen row in a full-screen picker was
private to OptionPicker, so every consumer hand-rolled its own copy.
Calendula alone carried five, and they had drifted: the kit drew
Icons.Rounded.Check, all five app copies drew Icons.Default.Check.

Make it public with KDoc so pickers share one selection affordance
instead of re-deriving it. Rounded wins, as the kit's existing choice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 15:17:29 +02:00
3 changed files with 51 additions and 10 deletions

View File

@@ -39,6 +39,8 @@ dependencies {
// core-locale backs the language-picker recipe (AppLanguage). // core-locale backs the language-picker recipe (AppLanguage).
implementation(project(":core-locale")) implementation(project(":core-locale"))
implementation(libs.androidx.core.ktx)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.androidx.foundation) implementation(libs.androidx.foundation)

View File

@@ -13,6 +13,8 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.view.WindowCompat
import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
/** /**
* Full-screen scaffold for selection pickers: a full-bleed [Dialog] that reuses * Full-screen scaffold for selection pickers: a full-bleed [Dialog] that reuses
@@ -41,14 +43,28 @@ fun FullScreenPicker(
decorFitsSystemWindows = false, decorFitsSystemWindows = false,
), ),
) { ) {
// The dialog window pans by default when the keyboard opens, which — // A Compose Dialog owns a separate android.view.Window whose bar-icon
// appearance is seeded from the XML theme, not from the activity window's
// runtime edge-to-edge state — so with decorFitsSystemWindows = false it
// draws edge-to-edge under bars whose icons don't match the active theme
// (dark icons on a dark picker). Drive the appearance from the runtime
// [LocalFloretDarkTheme] so it tracks the in-app light/dark choice even
// when that diverges from the system night qualifier.
//
// Same window also pans by default when the keyboard opens, which —
// combined with the content's own imePadding — leaves a fixed black gap // combined with the content's own imePadding — leaves a fixed black gap
// above the keyboard. Switch it to ADJUST_NOTHING so the window stays // above the keyboard. Switch it to ADJUST_NOTHING so the window stays
// full-screen and imePadding alone lifts the focused field. // full-screen and imePadding alone lifts the focused field.
val view = LocalView.current val view = LocalView.current
val darkTheme = LocalFloretDarkTheme.current
SideEffect { SideEffect {
(view.parent as? DialogWindowProvider)?.window (view.parent as? DialogWindowProvider)?.window?.let { window ->
?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
WindowCompat.getInsetsController(window, view).apply {
isAppearanceLightStatusBars = !darkTheme
isAppearanceLightNavigationBars = !darkTheme
}
}
} }
CollapsingScaffold( CollapsingScaffold(
title = title, title = title,
@@ -102,8 +118,18 @@ fun <T> OptionPicker(
} }
} }
/**
* The trailing check that marks the chosen row in a full-screen picker — the
* family's single selection affordance, so every picker (option, reminder,
* agenda range, timezone, calendar, recurrence) marks its selection the same
* way. Pair it with `GroupedRow(selected = true)`: the tonal highlight carries
* the state at a glance, the check confirms it unambiguously.
*
* Purely decorative — the row itself owns the semantics, so it takes no
* content description.
*/
@Composable @Composable
private fun SelectedCheck() { fun SelectedCheck() {
Icon( Icon(
imageVector = Icons.Rounded.Check, imageVector = Icons.Rounded.Check,
contentDescription = null, contentDescription = null,

View File

@@ -10,8 +10,19 @@ import androidx.compose.material3.Typography
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
/**
* The resolved light/dark decision the current [FloretExpressiveTheme] is running
* with, carried down the tree so components can align window chrome (e.g. status-
* and navigation-bar icon appearance) with the *runtime* theme rather than the
* system night resource qualifier — the two diverge whenever an app overrides
* light/dark from settings. Defaults to `false` (light) outside a Floret theme.
*/
val LocalFloretDarkTheme = staticCompositionLocalOf { false }
/** /**
* The family's Material 3 Expressive theme factory. Honours system light/dark * The family's Material 3 Expressive theme factory. Honours system light/dark
* and Material You dynamic colour (API 31+), falling back to the app-supplied * and Material You dynamic colour (API 31+), falling back to the app-supplied
@@ -43,10 +54,12 @@ fun FloretExpressiveTheme(
else -> lightScheme else -> lightScheme
} }
CompositionLocalProvider(LocalFloretDarkTheme provides darkTheme) {
MaterialExpressiveTheme( MaterialExpressiveTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = typography, typography = typography,
motionScheme = MotionScheme.standard(), motionScheme = MotionScheme.standard(),
content = content, content = content,
) )
}
} }