From c0299d27e3495efbb2d93fddcaace82259b90d01 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 02:23:02 +0530 Subject: [PATCH 01/11] WIP: relocate backdrop engine into internal package + add LiquidGlassBackdropBridge seam --- README.md | 14 +- .../liquidkit/build/LiquidKitConfiguration.kt | 4 +- .../navigation3/LiquidNav3TabScaffold.kt | 53 ++- .../liquidkit/internal/AndroidLiquidGlass.kt | 38 ++- .../LiquidGlassBackdropBridge.android.kt | 13 + .../androidglass}/backdrop/Backdrop.kt | 2 +- .../backdrop/BackdropEffectScope.kt | 2 +- .../backdrop/DrawBackdropModifier.kt | 16 +- .../backdrop/InverseLayerScope.kt | 2 +- .../androidglass}/backdrop/LayerRecorder.kt | 2 +- .../androidglass}/backdrop/Outline.kt | 2 +- .../backdrop/RuntimeShaderCache.kt | 2 +- .../androidglass}/backdrop/Shaders.kt | 2 +- .../androidglass}/backdrop/ShapeProvider.kt | 2 +- .../backdrop/backdrops/Backdrop.kt | 4 +- .../backdrop/backdrops/CanvasBackdrop.kt | 4 +- .../backdrop/backdrops/CombinedBackdrop.kt | 4 +- .../backdrop/backdrops/EmptyBackdrop.kt | 4 +- .../backdrop/backdrops/LayerBackdrop.kt | 6 +- .../backdrops/LayerBackdropModifier.kt | 4 +- .../androidglass}/backdrop/effects/Blur.kt | 4 +- .../backdrop/effects/ColorFilter.kt | 6 +- .../androidglass}/backdrop/effects/Lens.kt | 8 +- .../backdrop/effects/RenderEffect.kt | 4 +- .../backdrop/highlight/Highlight.kt | 2 +- .../backdrop/highlight/HighlightModifier.kt | 8 +- .../backdrop/highlight/HighlightStyle.kt | 8 +- .../backdrop/shadow/InnerShadow.kt | 2 +- .../backdrop/shadow/InnerShadowModifier.kt | 6 +- .../androidglass}/backdrop/shadow/Shadow.kt | 2 +- .../backdrop/shadow/ShadowModifier.kt | 4 +- .../catalog/components/LiquidBottomTabs.kt | 58 ++-- .../catalog/components/LiquidButton.kt | 57 ++-- .../catalog/components/LiquidSlider.kt | 138 ++++---- .../catalog/components/LiquidToggle.kt | 38 +-- .../LiquidBottomNavigation.android.kt | 69 +++- .../LiquidSegmentedControl.android.kt | 176 ++++++++-- .../liquidkit/slider/LiquidSlider.android.kt | 17 +- .../liquidkit/toggle/LiquidToggle.android.kt | 14 +- .../androidpoet/liquidkit/LiquidGlassStyle.kt | 29 ++ .../androidpoet/liquidkit/LiquidKitTheme.kt | 36 ++ .../androidpoet/liquidkit/icon/LiquidIcon.kt | 8 +- .../internal/InternalLiquidKitApi.kt | 13 + .../internal/LiquidGlassBackdropBridge.kt | 21 ++ .../navigation/LiquidNavigationItem.kt | 5 + .../liquidkit/navigation/LiquidTabScaffold.kt | 51 ++- .../segmented/LiquidSegmentedControl.kt | 6 +- .../liquidkit/slider/LiquidSlider.kt | 3 +- .../liquidkit/toggle/LiquidToggle.kt | 3 +- .../internal/LiquidGlassBackdropBridge.ios.kt | 9 + .../navigation/LiquidBottomNavigation.ios.kt | 5 + .../segmented/LiquidSegmentedControl.ios.kt | 13 +- .../liquidkit/toggle/LiquidToggle.ios.kt | 2 + .../kyant/backdrop/catalog/MainActivity.kt | 19 -- .../liquidkit/sample/MainActivity.kt | 306 +++++++++++++++++ .../liquidkit/sample/LiquidKitSampleApp.kt | 319 +++++++++++++++--- 56 files changed, 1255 insertions(+), 394 deletions(-) create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.android.kt rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/Backdrop.kt (85%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/BackdropEffectScope.kt (96%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/DrawBackdropModifier.kt (94%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/InverseLayerScope.kt (98%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/LayerRecorder.kt (91%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/Outline.kt (87%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/RuntimeShaderCache.kt (91%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/Shaders.kt (98%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/ShapeProvider.kt (94%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/backdrops/Backdrop.kt (86%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/backdrops/CanvasBackdrop.kt (83%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/backdrops/CombinedBackdrop.kt (94%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/backdrops/EmptyBackdrop.kt (79%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/backdrops/LayerBackdrop.kt (91%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/backdrops/LayerBackdropModifier.kt (91%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/effects/Blur.kt (85%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/effects/ColorFilter.kt (91%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/effects/Lens.kt (91%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/effects/RenderEffect.kt (77%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/highlight/Highlight.kt (89%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/highlight/HighlightModifier.kt (94%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/highlight/HighlightStyle.kt (93%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/shadow/InnerShadow.kt (94%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/shadow/InnerShadowModifier.kt (95%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/shadow/Shadow.kt (90%) rename liquidkit/src/androidMain/kotlin/{com/kyant => io/github/androidpoet/liquidkit/internal/androidglass}/backdrop/shadow/ShadowModifier.kt (96%) create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidKitTheme.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/internal/InternalLiquidKitApi.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.ios.kt delete mode 100644 sampleApp/src/androidMain/kotlin/com/kyant/backdrop/catalog/MainActivity.kt create mode 100644 sampleApp/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sample/MainActivity.kt diff --git a/README.md b/README.md index 294e526..53e1cc5 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,17 @@ LiquidTabScaffold(items = items) { selectedTab -> } ``` +Hide or replace only the bottom bar when another shell owns it: + +```kotlin +LiquidTabScaffold( + items = items, + bottomNavigation = { _, _ -> }, +) { selectedTab -> + TabContent(selectedTab) +} +``` + ## Navigation 3 Use `liquidkit-navigation3` only for Compose-owned bottom-navigation tab stacks. @@ -116,7 +127,8 @@ LiquidNav3TabScaffold( ``` For native iOS 26 Liquid Glass tabs, keep `TabView` in SwiftUI and host Compose -screens inside each tab. +screens inside each tab. If you reuse a Compose tab scaffold there, pass an empty +`bottomNavigation` slot so SwiftUI is the only tab bar. ## Build diff --git a/buildSrc/src/main/kotlin/io/github/androidpoet/liquidkit/build/LiquidKitConfiguration.kt b/buildSrc/src/main/kotlin/io/github/androidpoet/liquidkit/build/LiquidKitConfiguration.kt index 57fe20b..2558469 100644 --- a/buildSrc/src/main/kotlin/io/github/androidpoet/liquidkit/build/LiquidKitConfiguration.kt +++ b/buildSrc/src/main/kotlin/io/github/androidpoet/liquidkit/build/LiquidKitConfiguration.kt @@ -13,8 +13,8 @@ object LiquidKitConfiguration { const val libraryNamespace = "io.github.androidpoet.liquidkit" const val navigation3Namespace = "io.github.androidpoet.liquidkit.navigation3" - const val sampleNamespace = "com.kyant.backdrop.catalog" - const val sampleApplicationId = "com.kyant.backdrop.catalog" + const val sampleNamespace = "io.github.androidpoet.liquidkit.sample" + const val sampleApplicationId = "io.github.androidpoet.liquidkit.sample" const val libraryFrameworkName = "LiquidKit" const val sampleFrameworkName = "LiquidKitSample" diff --git a/liquidkit-navigation3/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3TabScaffold.kt b/liquidkit-navigation3/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3TabScaffold.kt index b33f002..d335cbf 100644 --- a/liquidkit-navigation3/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3TabScaffold.kt +++ b/liquidkit-navigation3/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3TabScaffold.kt @@ -3,6 +3,7 @@ package io.github.androidpoet.liquidkit.navigation3 import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -13,6 +14,10 @@ import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.captureToLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.rememberLiquidLayerBackdrop import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey @@ -193,6 +198,18 @@ public fun LiquidNav3TabScaffold( navigationModifier: Modifier = Modifier, style: LiquidGlassStyle = LiquidGlassStyle.NavigationBar, startRoute: NavKey = items.first().key, + bottomNavigation: @Composable BoxScope.( + state: LiquidNav3TabState, + onSelected: (NavKey) -> Unit, + ) -> Unit = { state, onSelected -> + LiquidBottomNavigation( + items = items, + selectedKey = state.selectedRoute, + onSelected = onSelected, + modifier = navigationModifier.align(Alignment.BottomCenter), + style = style, + ) + }, ) { val state = rememberLiquidNav3TabState( items = items, @@ -207,9 +224,11 @@ public fun LiquidNav3TabScaffold( modifier = modifier, navigationModifier = navigationModifier, style = style, + bottomNavigation = bottomNavigation, ) } +@OptIn(InternalLiquidKitApi::class) @Composable public fun LiquidNav3TabScaffold( items: List>, @@ -218,22 +237,36 @@ public fun LiquidNav3TabScaffold( modifier: Modifier = Modifier, navigationModifier: Modifier = Modifier, style: LiquidGlassStyle = LiquidGlassStyle.NavigationBar, -) { - require(items.isNotEmpty()) { "LiquidNav3TabScaffold requires at least one item." } - - Box(modifier = modifier) { - LiquidNav3TabContent( - state = state, - entryProvider = entryProvider, - modifier = Modifier.matchParentSize(), - ) + bottomNavigation: @Composable BoxScope.( + state: LiquidNav3TabState, + onSelected: (NavKey) -> Unit, + ) -> Unit = { state, onSelected -> LiquidBottomNavigation( items = items, selectedKey = state.selectedRoute, - onSelected = state::select, + onSelected = onSelected, modifier = navigationModifier.align(Alignment.BottomCenter), style = style, ) + }, +) { + require(items.isNotEmpty()) { "LiquidNav3TabScaffold requires at least one item." } + + val layerBackdrop = rememberLiquidLayerBackdrop() + Box(modifier = modifier) { + // Content records into the layer — must NOT read from it or a circular GraphicsLayer + // reference forms (content-capture layer → glass component inside → same layer → ∞). + Box(Modifier.matchParentSize().captureToLiquidLayerBackdrop(layerBackdrop)) { + LiquidNav3TabContent( + state = state, + entryProvider = entryProvider, + modifier = Modifier.matchParentSize(), + ) + } + // Nav bar is scoped to the layer so it can blur the content behind it. + CompositionLocalProvider(LocalLiquidLayerBackdrop provides layerBackdrop) { + bottomNavigation(state, state::select) + } } } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt index 8b5bd3a..c057f08 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt @@ -1,40 +1,47 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + package io.github.androidpoet.liquidkit.internal -import androidx.compose.foundation.border import androidx.compose.ui.Modifier import androidx.compose.ui.composed -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens -import io.github.androidpoet.liquidkit.LiquidGlassStyle -import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight /* - * Android Liquid Glass renderer backed by vendored source copied from: + * Android Liquid Glass renderer backed by vendored source from: * https://github.com/Kyant0/AndroidLiquidGlass */ internal fun Modifier.androidLiquidGlass( shape: Shape, style: LiquidGlassStyle, ): Modifier = composed { - val backdrop = rememberCanvasBackdrop { - drawRect(style.containerColor) - } + // Use a layer backdrop from the composition local if a scaffold has set one up, + // otherwise fall back to an empty canvas backdrop (lens / highlight still apply). + val layerCapture = LocalLiquidLayerBackdrop.current + val emptyBackdrop = rememberCanvasBackdrop {} + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else emptyBackdrop drawBackdrop( backdrop = backdrop, shape = { shape }, effects = { + vibrancy() blur(style.androidBlurRadius.toPx()) - lens(style.androidRefractionHeight.toPx(), style.androidRefractionHeight.toPx()) + lens(style.androidRefractionHeight.toPx(), style.androidRefractionAmount.toPx()) }, + highlight = { Highlight.Default }, onDrawSurface = { drawRect(style.containerColor) }, ) - .border(1.dp, Color.White.copy(alpha = 0.34f), shape) } private val LiquidGlassStyle.androidBlurRadius: Dp @@ -52,3 +59,12 @@ private val LiquidGlassStyle.androidRefractionHeight: Dp LiquidGlassStyle.Control -> 8.dp else -> (cornerRadius * 0.48f).coerceIn(6.dp, 16.dp) } + +// Refraction amount is roughly 2× height for a pronounced glass-edge distortion. +private val LiquidGlassStyle.androidRefractionAmount: Dp + get() = when (this) { + LiquidGlassStyle.Surface -> 20.dp + LiquidGlassStyle.NavigationBar -> 28.dp + LiquidGlassStyle.Control -> 16.dp + else -> (cornerRadius * 0.96f).coerceIn(12.dp, 32.dp) + } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.android.kt new file mode 100644 index 0000000..5ec0ce6 --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.android.kt @@ -0,0 +1,13 @@ +package io.github.androidpoet.liquidkit.internal + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop + +@Composable +public actual fun rememberLiquidLayerBackdrop(): Any? = rememberLayerBackdrop() + +public actual fun Modifier.captureToLiquidLayerBackdrop(backdrop: Any?): Modifier = + if (backdrop is LayerBackdrop) layerBackdrop(backdrop) else this diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/Backdrop.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/Backdrop.kt similarity index 85% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/Backdrop.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/Backdrop.kt index a617938..55a2fbe 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/Backdrop.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/Backdrop.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.graphics.drawscope.DrawScope diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/BackdropEffectScope.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/BackdropEffectScope.kt similarity index 96% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/BackdropEffectScope.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/BackdropEffectScope.kt index a271d45..d03a528 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/BackdropEffectScope.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/BackdropEffectScope.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop import android.graphics.RenderEffect import android.graphics.RuntimeShader diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/DrawBackdropModifier.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/DrawBackdropModifier.kt similarity index 94% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/DrawBackdropModifier.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/DrawBackdropModifier.kt index e970c24..ae43f69 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/DrawBackdropModifier.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/DrawBackdropModifier.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop import android.os.Build import androidx.compose.runtime.getValue @@ -30,13 +30,13 @@ import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize -import com.kyant.backdrop.backdrops.LayerBackdrop -import com.kyant.backdrop.highlight.Highlight -import com.kyant.backdrop.highlight.HighlightElement -import com.kyant.backdrop.shadow.InnerShadow -import com.kyant.backdrop.shadow.InnerShadowElement -import com.kyant.backdrop.shadow.Shadow -import com.kyant.backdrop.shadow.ShadowElement +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.HighlightElement +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadowElement +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.ShadowElement private val DefaultHighlight = { Highlight.Default } private val DefaultShadow = { Shadow.Default } diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/InverseLayerScope.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/InverseLayerScope.kt similarity index 98% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/InverseLayerScope.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/InverseLayerScope.kt index 25941d0..4cc25eb 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/InverseLayerScope.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/InverseLayerScope.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/LayerRecorder.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/LayerRecorder.kt similarity index 91% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/LayerRecorder.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/LayerRecorder.kt index 82623b0..c202552 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/LayerRecorder.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/LayerRecorder.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.layer.GraphicsLayer diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/Outline.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/Outline.kt similarity index 87% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/Outline.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/Outline.kt index 9220e46..13e77b0 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/Outline.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/Outline.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Outline diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/RuntimeShaderCache.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/RuntimeShaderCache.kt similarity index 91% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/RuntimeShaderCache.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/RuntimeShaderCache.kt index f4a953b..4527055 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/RuntimeShaderCache.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/RuntimeShaderCache.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop import android.graphics.RuntimeShader import android.os.Build diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/Shaders.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/Shaders.kt similarity index 98% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/Shaders.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/Shaders.kt index 73fdcfe..205b8d0 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/Shaders.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/Shaders.kt @@ -14,7 +14,7 @@ limitations under the License. */ -package com.kyant.backdrop +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop import org.intellij.lang.annotations.Language diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/ShapeProvider.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/ShapeProvider.kt similarity index 94% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/ShapeProvider.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/ShapeProvider.kt index c79a776..3985725 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/ShapeProvider.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/ShapeProvider.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop import androidx.compose.runtime.Immutable import androidx.compose.ui.geometry.Size diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/Backdrop.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/Backdrop.kt similarity index 86% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/Backdrop.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/Backdrop.kt index 92bf6ea..c335d0f 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/Backdrop.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/Backdrop.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.backdrops +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable @@ -7,7 +7,7 @@ import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.Density -import com.kyant.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop @Composable fun rememberBackdrop( diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/CanvasBackdrop.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/CanvasBackdrop.kt similarity index 83% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/CanvasBackdrop.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/CanvasBackdrop.kt index 74156eb..c174cba 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/CanvasBackdrop.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/CanvasBackdrop.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.backdrops +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable @@ -7,7 +7,7 @@ import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.Density -import com.kyant.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop @Composable fun rememberCanvasBackdrop( diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/CombinedBackdrop.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/CombinedBackdrop.kt similarity index 94% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/CombinedBackdrop.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/CombinedBackdrop.kt index 943d3d7..891e4d4 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/CombinedBackdrop.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/CombinedBackdrop.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.backdrops +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable @@ -7,7 +7,7 @@ import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.Density -import com.kyant.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop @Composable fun rememberCombinedBackdrop( diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/EmptyBackdrop.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/EmptyBackdrop.kt similarity index 79% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/EmptyBackdrop.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/EmptyBackdrop.kt index 7701a8c..d158b64 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/EmptyBackdrop.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/EmptyBackdrop.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.backdrops +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable @@ -6,7 +6,7 @@ import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.Density -import com.kyant.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop @Stable fun emptyBackdrop(): Backdrop = EmptyBackdrop diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/LayerBackdrop.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/LayerBackdrop.kt similarity index 91% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/LayerBackdrop.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/LayerBackdrop.kt index 7e15110..9a1c98f 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/LayerBackdrop.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/LayerBackdrop.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.backdrops +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -16,8 +16,8 @@ import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.unit.Density -import com.kyant.backdrop.Backdrop -import com.kyant.backdrop.InverseLayerScope +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.InverseLayerScope private val DefaultOnDraw: ContentDrawScope.() -> Unit = { drawContent() } diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/LayerBackdropModifier.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/LayerBackdropModifier.kt similarity index 91% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/LayerBackdropModifier.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/LayerBackdropModifier.kt index a0251cf..e163307 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/backdrops/LayerBackdropModifier.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/backdrops/LayerBackdropModifier.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.backdrops +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.ContentDrawScope @@ -8,7 +8,7 @@ import androidx.compose.ui.node.GlobalPositionAwareModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.invalidateDraw import androidx.compose.ui.platform.InspectorInfo -import com.kyant.backdrop.recordLayer +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.recordLayer fun Modifier.layerBackdrop(backdrop: LayerBackdrop): Modifier = this then LayerBackdropElement(backdrop) diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/Blur.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/Blur.kt similarity index 85% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/Blur.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/Blur.kt index d3599f3..6c38c84 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/Blur.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/Blur.kt @@ -1,11 +1,11 @@ -package com.kyant.backdrop.effects +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects import android.graphics.RenderEffect import android.os.Build import androidx.annotation.FloatRange import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.graphics.toAndroidTileMode -import com.kyant.backdrop.BackdropEffectScope +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.BackdropEffectScope fun BackdropEffectScope.blur( @FloatRange(from = 0.0) radius: Float, diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/ColorFilter.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/ColorFilter.kt similarity index 91% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/ColorFilter.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/ColorFilter.kt index 57595ee..9368f39 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/ColorFilter.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/ColorFilter.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.effects +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects import android.graphics.ColorFilter import android.graphics.ColorMatrix @@ -7,8 +7,8 @@ import android.graphics.RenderEffect import android.os.Build import androidx.annotation.FloatRange import androidx.compose.ui.graphics.asAndroidColorFilter -import com.kyant.backdrop.BackdropEffectScope -import com.kyant.backdrop.GammaAdjustmentShaderString +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.BackdropEffectScope +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.GammaAdjustmentShaderString import kotlin.math.pow fun BackdropEffectScope.colorFilter(colorFilter: ColorFilter) { diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/Lens.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/Lens.kt similarity index 91% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/Lens.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/Lens.kt index 93e55de..da0281a 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/Lens.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/Lens.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.effects +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects import android.graphics.RenderEffect import android.os.Build @@ -8,9 +8,9 @@ import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastCoerceAtMost -import com.kyant.backdrop.BackdropEffectScope -import com.kyant.backdrop.RoundedRectRefractionShaderString -import com.kyant.backdrop.RoundedRectRefractionWithDispersionShaderString +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.BackdropEffectScope +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.RoundedRectRefractionShaderString +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.RoundedRectRefractionWithDispersionShaderString import com.kyant.shapes.RoundedRectangularShape fun BackdropEffectScope.lens( diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/RenderEffect.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/RenderEffect.kt similarity index 77% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/RenderEffect.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/RenderEffect.kt index bccbb90..d1e99db 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/effects/RenderEffect.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/effects/RenderEffect.kt @@ -1,8 +1,8 @@ -package com.kyant.backdrop.effects +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects import android.graphics.RenderEffect import android.os.Build -import com.kyant.backdrop.BackdropEffectScope +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.BackdropEffectScope fun BackdropEffectScope.effect(effect: RenderEffect) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/highlight/Highlight.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/highlight/Highlight.kt similarity index 89% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/highlight/Highlight.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/highlight/Highlight.kt index eda956f..c083cd2 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/highlight/Highlight.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/highlight/Highlight.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.highlight +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight import androidx.annotation.FloatRange import androidx.compose.runtime.Immutable diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/highlight/HighlightModifier.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/highlight/HighlightModifier.kt similarity index 94% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/highlight/HighlightModifier.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/highlight/HighlightModifier.kt index fdd66fa..2e3b9f3 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/highlight/HighlightModifier.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/highlight/HighlightModifier.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.highlight +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight import android.graphics.BlurMaskFilter import android.os.Build @@ -21,9 +21,9 @@ import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastCoerceAtMost -import com.kyant.backdrop.RuntimeShaderCacheImpl -import com.kyant.backdrop.ShapeProvider -import com.kyant.backdrop.clipOutline +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.RuntimeShaderCacheImpl +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.ShapeProvider +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.clipOutline import kotlin.math.ceil internal class HighlightElement( diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/highlight/HighlightStyle.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/highlight/HighlightStyle.kt similarity index 93% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/highlight/HighlightStyle.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/highlight/HighlightStyle.kt index 8b7934d..c959707 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/highlight/HighlightStyle.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/highlight/HighlightStyle.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.highlight +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight import android.os.Build import androidx.annotation.FloatRange @@ -14,9 +14,9 @@ import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastCoerceAtMost -import com.kyant.backdrop.AmbientHighlightShaderString -import com.kyant.backdrop.DefaultHighlightShaderString -import com.kyant.backdrop.RuntimeShaderCache +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.AmbientHighlightShaderString +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.DefaultHighlightShaderString +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.RuntimeShaderCache import kotlin.math.PI @Immutable diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/InnerShadow.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/InnerShadow.kt similarity index 94% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/InnerShadow.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/InnerShadow.kt index 1475bec..4a042be 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/InnerShadow.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/InnerShadow.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.shadow +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow import androidx.annotation.FloatRange import androidx.compose.runtime.Immutable diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/InnerShadowModifier.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/InnerShadowModifier.kt similarity index 95% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/InnerShadowModifier.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/InnerShadowModifier.kt index 4c65d28..1690c86 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/InnerShadowModifier.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/InnerShadowModifier.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.shadow +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow import android.os.Build import androidx.compose.ui.Modifier @@ -20,8 +20,8 @@ import androidx.compose.ui.node.invalidateDraw import androidx.compose.ui.node.requireGraphicsContext import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.Density -import com.kyant.backdrop.ShapeProvider -import com.kyant.backdrop.clipOutline +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.ShapeProvider +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.clipOutline internal class InnerShadowElement( val shapeProvider: ShapeProvider, diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/Shadow.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/Shadow.kt similarity index 90% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/Shadow.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/Shadow.kt index 24684f8..df021c0 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/Shadow.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/Shadow.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.shadow +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow import androidx.annotation.FloatRange import androidx.compose.runtime.Immutable diff --git a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/ShadowModifier.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/ShadowModifier.kt similarity index 96% rename from liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/ShadowModifier.kt rename to liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/ShadowModifier.kt index 1d810dd..62d7818 100644 --- a/liquidkit/src/androidMain/kotlin/com/kyant/backdrop/shadow/ShadowModifier.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/backdrop/shadow/ShadowModifier.kt @@ -1,4 +1,4 @@ -package com.kyant.backdrop.shadow +package io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow import android.graphics.BlurMaskFilter import androidx.compose.ui.Modifier @@ -18,7 +18,7 @@ import androidx.compose.ui.node.requireGraphicsContext import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize -import com.kyant.backdrop.ShapeProvider +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.ShapeProvider import kotlin.math.ceil internal class ShadowElement( diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidBottomTabs.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidBottomTabs.kt index 38f5c94..63e0493 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidBottomTabs.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidBottomTabs.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.clearAndSetSemantics @@ -37,19 +36,19 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastRoundToInt import androidx.compose.ui.util.lerp -import com.kyant.backdrop.Backdrop -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberCombinedBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCombinedBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.catalog.utils.DampedDragAnimation import io.github.androidpoet.liquidkit.internal.androidglass.catalog.utils.InteractiveHighlight -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.blur -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.effects.vibrancy -import com.kyant.backdrop.highlight.Highlight -import com.kyant.backdrop.shadow.InnerShadow -import com.kyant.backdrop.shadow.Shadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow import com.kyant.shapes.Capsule import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop @@ -64,19 +63,14 @@ fun LiquidBottomTabs( backdrop: Backdrop, tabsCount: Int, modifier: Modifier = Modifier, - accentColor: Color = Color.Unspecified, - containerColor: Color = Color.Unspecified, - selectedSurfaceColor: Color = Color.Unspecified, content: @Composable RowScope.() -> Unit ) { val isLightTheme = !isSystemInDarkTheme() - val resolvedAccentColor = - if (accentColor.isSpecified) accentColor - else if (isLightTheme) Color(0xFF0088FF) + val accentColor = + if (isLightTheme) Color(0xFF0088FF) else Color(0xFF0091FF) - val resolvedContainerColor = - if (containerColor.isSpecified) containerColor - else if (isLightTheme) Color(0xFFFAFAFA).copy(0.4f) + val containerColor = + if (isLightTheme) Color(0xFFFAFAFA).copy(0.4f) else Color(0xFF121212).copy(0.4f) val tabsBackdrop = rememberLayerBackdrop() @@ -102,7 +96,7 @@ fun LiquidBottomTabs( val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr val animationScope = rememberCoroutineScope() - var currentIndex by remember { + var currentIndex by remember(selectedTabIndex) { mutableIntStateOf(selectedTabIndex()) } val dampedDragAnimation = remember(animationScope) { @@ -183,7 +177,7 @@ fun LiquidBottomTabs( scaleX = scale scaleY = scale }, - onDrawSurface = { drawRect(resolvedContainerColor) } + onDrawSurface = { drawRect(containerColor) } ) .then(interactiveHighlight.modifier) .height(64f.dp) @@ -222,13 +216,13 @@ fun LiquidBottomTabs( val progress = dampedDragAnimation.pressProgress Highlight.Default.copy(alpha = progress) }, - onDrawSurface = { drawRect(resolvedContainerColor) } + onDrawSurface = { drawRect(containerColor) } ) .then(interactiveHighlight.modifier) .height(56f.dp) .fillMaxWidth() .padding(horizontal = 4f.dp) - .graphicsLayer(colorFilter = ColorFilter.tint(resolvedAccentColor)), + .graphicsLayer(colorFilter = ColorFilter.tint(accentColor)), verticalAlignment = Alignment.CenterVertically, content = content ) @@ -279,15 +273,11 @@ fun LiquidBottomTabs( }, onDrawSurface = { val progress = dampedDragAnimation.pressProgress - if (selectedSurfaceColor.isSpecified) { - drawRect(selectedSurfaceColor, alpha = 1f - progress) - } else { - drawRect( - if (isLightTheme) Color.Black.copy(0.1f) - else Color.White.copy(0.1f), - alpha = 1f - progress - ) - } + drawRect( + if (isLightTheme) Color.Black.copy(0.1f) + else Color.White.copy(0.1f), + alpha = 1f - progress + ) drawRect(Color.Black.copy(alpha = 0.03f * progress)) } ) diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidButton.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidButton.kt index cac1a5b..384fecb 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidButton.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidButton.kt @@ -19,13 +19,13 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceAtMost import androidx.compose.ui.util.lerp -import com.kyant.backdrop.Backdrop -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.blur -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.effects.vibrancy -import com.kyant.shapes.Capsule +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop import io.github.androidpoet.liquidkit.internal.androidglass.catalog.utils.InteractiveHighlight +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy +import com.kyant.shapes.Capsule import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.cos @@ -37,15 +37,17 @@ fun LiquidButton( onClick: () -> Unit, backdrop: Backdrop, modifier: Modifier = Modifier, - enabled: Boolean = true, isInteractive: Boolean = true, tint: Color = Color.Unspecified, surfaceColor: Color = Color.Unspecified, - content: @Composable RowScope.() -> Unit, + content: @Composable RowScope.() -> Unit ) { val animationScope = rememberCoroutineScope() + val interactiveHighlight = remember(animationScope) { - InteractiveHighlight(animationScope = animationScope) + InteractiveHighlight( + animationScope = animationScope + ) } Row( @@ -58,28 +60,30 @@ fun LiquidButton( blur(2f.dp.toPx()) lens(12f.dp.toPx(), 24f.dp.toPx()) }, - layerBlock = if (isInteractive && enabled) { + layerBlock = if (isInteractive) { { val width = size.width val height = size.height + val progress = interactiveHighlight.pressProgress val scale = lerp(1f, 1f + 4f.dp.toPx() / size.height, progress) + val maxOffset = size.minDimension - val offset = interactiveHighlight.offset - val offsetAngle = atan2(offset.y, offset.x) val initialDerivative = 0.05f - val maxDragScale = 4f.dp.toPx() / size.height - + val offset = interactiveHighlight.offset translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset) translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset) + + val maxDragScale = 4f.dp.toPx() / size.height + val offsetAngle = atan2(offset.y, offset.x) scaleX = scale + - maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) * - (width / height).fastCoerceAtMost(1f) + maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) * + (width / height).fastCoerceAtMost(1f) scaleY = scale + - maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) * - (height / width).fastCoerceAtMost(1f) + maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) * + (height / width).fastCoerceAtMost(1f) } } else { null @@ -92,28 +96,27 @@ fun LiquidButton( if (surfaceColor.isSpecified) { drawRect(surfaceColor) } - }, + } ) .clickable( interactionSource = null, indication = if (isInteractive) null else LocalIndication.current, - enabled = enabled, role = Role.Button, - onClick = onClick, + onClick = onClick ) .then( - if (isInteractive && enabled) { + if (isInteractive) { Modifier .then(interactiveHighlight.modifier) .then(interactiveHighlight.gestureModifier) } else { Modifier - }, + } ) - .height(48.dp) - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + .height(48f.dp) + .padding(horizontal = 16f.dp), + horizontalArrangement = Arrangement.spacedBy(8f.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, - content = content, + content = content ) } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidSlider.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidSlider.kt index 1b8f2de..66dd2ad 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidSlider.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidSlider.kt @@ -1,6 +1,7 @@ package io.github.androidpoet.liquidkit.internal.androidglass.catalog.components import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -21,7 +22,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalLayoutDirection @@ -30,20 +30,19 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastRoundToInt import androidx.compose.ui.util.lerp -import com.kyant.backdrop.Backdrop -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberBackdrop -import com.kyant.backdrop.backdrops.rememberCombinedBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.blur -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.highlight.Highlight -import com.kyant.backdrop.shadow.InnerShadow -import com.kyant.backdrop.shadow.Shadow -import com.kyant.shapes.Capsule +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCombinedBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.catalog.utils.DampedDragAnimation -import io.github.androidpoet.liquidkit.internal.androidglass.catalog.utils.inspectDragGestures +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow +import com.kyant.shapes.Capsule import kotlinx.coroutines.flow.collectLatest @Composable @@ -53,29 +52,24 @@ fun LiquidSlider( valueRange: ClosedFloatingPointRange, visibilityThreshold: Float, backdrop: Backdrop, - modifier: Modifier = Modifier, - enabled: Boolean = true, - activeTrackColor: Color = Color.Unspecified, - inactiveTrackColor: Color = Color.Unspecified, - thumbColor: Color = Color.White, + modifier: Modifier = Modifier ) { val isLightTheme = !isSystemInDarkTheme() - val resolvedActiveTrackColor = - if (activeTrackColor.isSpecified) activeTrackColor - else if (isLightTheme) Color(0xFF0088FF) + val accentColor = + if (isLightTheme) Color(0xFF0088FF) else Color(0xFF0091FF) - val resolvedInactiveTrackColor = - if (inactiveTrackColor.isSpecified) inactiveTrackColor - else if (isLightTheme) Color(0xFF787878).copy(0.2f) + val trackColor = + if (isLightTheme) Color(0xFF787878).copy(0.2f) else Color(0xFF787880).copy(0.36f) val trackBackdrop = rememberLayerBackdrop() BoxWithConstraints( modifier.fillMaxWidth(), - contentAlignment = Alignment.CenterStart, + contentAlignment = Alignment.CenterStart ) { val trackWidth = constraints.maxWidth + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr val animationScope = rememberCoroutineScope() var didDrag by remember { mutableStateOf(false) } @@ -89,23 +83,20 @@ fun LiquidSlider( pressedScale = 1.5f, onDragStarted = {}, onDragStopped = { - if (didDrag && enabled) { + if (didDrag) { onValueChange(targetValue) } - didDrag = false }, onDrag = { _, dragAmount -> - if (enabled) { - if (!didDrag) { - didDrag = dragAmount.x != 0f - } - val delta = (valueRange.endInclusive - valueRange.start) * (dragAmount.x / trackWidth) - onValueChange( - if (isLtr) (targetValue + delta).coerceIn(valueRange) - else (targetValue - delta).coerceIn(valueRange), - ) + if (!didDrag) { + didDrag = dragAmount.x != 0f } - }, + val delta = (valueRange.endInclusive - valueRange.start) * (dragAmount.x / trackWidth) + onValueChange( + if (isLtr) (targetValue + delta).coerceIn(valueRange) + else (targetValue - delta).coerceIn(valueRange) + ) + } ) } LaunchedEffect(dampedDragAnimation) { @@ -116,57 +107,42 @@ fun LiquidSlider( } } } - fun updateFromPosition(positionX: Float) { - val progress = (positionX / trackWidth).fastCoerceIn(0f, 1f) - val targetValue = - if (isLtr) { - valueRange.start + (valueRange.endInclusive - valueRange.start) * progress - } else { - valueRange.endInclusive - (valueRange.endInclusive - valueRange.start) * progress - } - val coercedValue = targetValue.coerceIn(valueRange) - dampedDragAnimation.updateValue(coercedValue) - onValueChange(coercedValue) - } Box(Modifier.layerBackdrop(trackBackdrop)) { Box( Modifier .clip(Capsule()) - .background(resolvedInactiveTrackColor) - .height(6.dp) - .fillMaxWidth(), + .background(trackColor) + .pointerInput(animationScope) { + detectTapGestures { position -> + val delta = (valueRange.endInclusive - valueRange.start) * (position.x / trackWidth) + val targetValue = + (if (isLtr) valueRange.start + delta + else valueRange.endInclusive - delta) + .coerceIn(valueRange) + dampedDragAnimation.animateToValue(targetValue) + onValueChange(targetValue) + } + } + .height(6f.dp) + .fillMaxWidth() ) Box( Modifier .clip(Capsule()) - .background(resolvedActiveTrackColor) - .height(6.dp) + .background(accentColor) + .height(6f.dp) .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val width = (constraints.maxWidth * dampedDragAnimation.progress).fastRoundToInt() layout(width, placeable.height) { placeable.place(0, 0) } - }, + } ) } - Box( - Modifier - .height(48.dp) - .fillMaxWidth() - .pointerInput(enabled, trackWidth, isLtr, valueRange) { - if (enabled) { - inspectDragGestures( - onDragStart = { down -> updateFromPosition(down.position.x) }, - onDrag = { change, _ -> updateFromPosition(change.position.x) }, - ) - } - }, - ) - Box( Modifier .graphicsLayer { @@ -174,7 +150,7 @@ fun LiquidSlider( (-size.width / 2f + trackWidth * dampedDragAnimation.progress) .fastCoerceIn(-size.width / 4f, trackWidth - size.width * 3f / 4f) * if (isLtr) 1f else -1f } - .then(if (enabled) dampedDragAnimation.modifier else Modifier) + .then(dampedDragAnimation.modifier) .drawBackdrop( backdrop = rememberCombinedBackdrop( backdrop, @@ -185,7 +161,7 @@ fun LiquidSlider( scale(scaleX, scaleY) { drawBackdrop() } - }, + } ), shape = { Capsule() }, effects = { @@ -194,7 +170,7 @@ fun LiquidSlider( lens( 10f.dp.toPx() * progress, 14f.dp.toPx() * progress, - chromaticAberration = true, + chromaticAberration = true ) }, highlight = { @@ -202,20 +178,20 @@ fun LiquidSlider( Highlight.Ambient.copy( width = Highlight.Ambient.width / 1.5f, blurRadius = Highlight.Ambient.blurRadius / 1.5f, - alpha = progress, + alpha = progress ) }, shadow = { Shadow( - radius = 4.dp, - color = Color.Black.copy(alpha = 0.05f), + radius = 4f.dp, + color = Color.Black.copy(alpha = 0.05f) ) }, innerShadow = { val progress = dampedDragAnimation.pressProgress InnerShadow( - radius = 4.dp * progress, - alpha = progress, + radius = 4f.dp * progress, + alpha = progress ) }, layerBlock = { @@ -227,10 +203,10 @@ fun LiquidSlider( }, onDrawSurface = { val progress = dampedDragAnimation.pressProgress - drawRect(thumbColor.copy(alpha = 1f - progress)) - }, + drawRect(Color.White.copy(alpha = 1f - progress)) + } ) - .size(40.dp, 24.dp), + .size(40f.dp, 24f.dp) ) } } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidToggle.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidToggle.kt index ca565fb..41340f0 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidToggle.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/androidglass/catalog/components/LiquidToggle.kt @@ -1,6 +1,5 @@ package io.github.androidpoet.liquidkit.internal.androidglass.catalog.components -import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size @@ -30,18 +29,18 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.lerp -import com.kyant.backdrop.Backdrop -import com.kyant.backdrop.backdrops.layerBackdrop -import com.kyant.backdrop.backdrops.rememberBackdrop -import com.kyant.backdrop.backdrops.rememberCombinedBackdrop -import com.kyant.backdrop.backdrops.rememberLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCombinedBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.catalog.utils.DampedDragAnimation -import com.kyant.backdrop.drawBackdrop -import com.kyant.backdrop.effects.blur -import com.kyant.backdrop.effects.lens -import com.kyant.backdrop.highlight.Highlight -import com.kyant.backdrop.shadow.InnerShadow -import com.kyant.backdrop.shadow.Shadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow import com.kyant.shapes.Capsule import kotlinx.coroutines.flow.collectLatest @@ -50,8 +49,7 @@ fun LiquidToggle( selected: () -> Boolean, onSelect: (Boolean) -> Unit, backdrop: Backdrop, - modifier: Modifier = Modifier, - enabled: Boolean = true, + modifier: Modifier = Modifier ) { val isLightTheme = !isSystemInDarkTheme() val accentColor = @@ -115,11 +113,6 @@ fun LiquidToggle( } val trackBackdrop = rememberLayerBackdrop() - fun toggle() { - val isSelected = !selected() - fraction = if (isSelected) 1f else 0f - onSelect(isSelected) - } Box( modifier, @@ -129,13 +122,6 @@ fun LiquidToggle( Modifier .layerBackdrop(trackBackdrop) .clip(Capsule()) - .clickable( - interactionSource = null, - indication = null, - enabled = enabled, - role = Role.Switch, - onClick = ::toggle, - ) .drawBehind { val fraction = dampedDragAnimation.value drawRect(lerp(trackColor, accentColor, fraction)) diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.android.kt index 22a3742..af4d17e 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.android.kt @@ -1,15 +1,23 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + package io.github.androidpoet.liquidkit.navigation import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.text.TextStyle @@ -17,7 +25,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.kyant.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidBottomTab import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidBottomTabs import io.github.androidpoet.liquidkit.LiquidGlassStyle @@ -30,9 +41,9 @@ internal actual fun PlatformLiquidBottomNavigation( modifier: Modifier, style: LiquidGlassStyle, ) { - val backdrop = rememberCanvasBackdrop { - drawRect(style.containerColor) - } + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop val selectedIndex = items.indexOfFirst { it.key == selectedKey }.coerceAtLeast(0) LiquidBottomTabs( @@ -70,13 +81,21 @@ private fun LiquidBottomNavigationItem( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), ) { - item.icon?.let { icon -> - Image( - painter = rememberVectorPainter(icon.vectorFor(selected)), - contentDescription = icon.contentDescription, - modifier = Modifier.size(20.dp), - colorFilter = ColorFilter.tint(contentColor), - ) + item.icon?.vectorFor(selected)?.let { vector -> + Box { + Image( + painter = rememberVectorPainter(vector), + contentDescription = item.icon.contentDescription, + modifier = Modifier.size(20.dp), + colorFilter = ColorFilter.tint(contentColor), + ) + item.badge?.let { badge -> + BadgeIndicator( + badge = badge, + modifier = Modifier.align(Alignment.TopEnd).offset(x = 4.dp, y = (-2).dp), + ) + } + } } BasicText( @@ -91,3 +110,31 @@ private fun LiquidBottomNavigationItem( ) } } + +@Composable +private fun BadgeIndicator(badge: Int, modifier: Modifier = Modifier) { + if (badge == 0) { + Box( + modifier = modifier + .size(8.dp) + .background(Color(0xFFFF3B30), CircleShape), + ) + } else { + val count = badge.coerceAtMost(99) + Box( + modifier = modifier + .background(Color(0xFFFF3B30), CircleShape) + .padding(horizontal = 3.dp, vertical = 1.dp), + contentAlignment = Alignment.Center, + ) { + BasicText( + text = count.toString(), + style = TextStyle( + color = Color.White, + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + ), + ) + } + } +} diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.android.kt index 7536638..b34961d 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.android.kt @@ -1,17 +1,54 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + package io.github.androidpoet.liquidkit.segmented +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.kyant.backdrop.backdrops.rememberCanvasBackdrop +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.rememberVectorPainter import io.github.androidpoet.liquidkit.LiquidGlassStyle -import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidBottomTab -import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidBottomTabs +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCombinedBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow +import com.kyant.shapes.Capsule @Composable internal actual fun PlatformLiquidSegmentedControl( @@ -22,41 +59,114 @@ internal actual fun PlatformLiquidSegmentedControl( enabled: Boolean, style: LiquidGlassStyle, ) { - val backdrop = rememberCanvasBackdrop { - drawRect(style.containerColor) - } + val isLight = !isSystemInDarkTheme() + val containerColor = if (isLight) Color(0xFFFAFAFA).copy(0.4f) else Color(0xFF121212).copy(0.4f) + + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val outerBackdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + val segCount = segments.size val selectedIndex = segments.indexOfFirst { it.key == selectedKey }.coerceAtLeast(0) - LiquidBottomTabs( - selectedTabIndex = { selectedIndex }, - onTabSelected = { index -> - if (enabled) segments.getOrNull(index)?.key?.let(onSelected) - }, - backdrop = backdrop, - tabsCount = segments.size, - modifier = modifier.alpha(if (enabled) 1f else 0.42f), - accentColor = style.selectedContentColor, - containerColor = style.containerColor, - selectedSurfaceColor = style.selectedContainerColor, + // Track layer: the container pill captures itself for the indicator to refract over + val trackLayer = rememberLayerBackdrop() + + BoxWithConstraints( + modifier + .alpha(if (enabled) 1f else 0.42f) + .fillMaxWidth(), + contentAlignment = Alignment.CenterStart, ) { - segments.forEach { segment -> - val selected = segment.key == selectedKey - LiquidBottomTab( - onClick = { - if (enabled) onSelected(segment.key) - }, - ) { - BasicText( - text = segment.label, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TextStyle( - color = if (selected) style.selectedContentColor else style.contentColor, - fontSize = 13.sp, - fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, - ), + val segWidth = constraints.maxWidth.toFloat() / segCount + + // Animate indicator position; LaunchedEffect re-runs on every selectedIndex change + val indicatorOffset = remember { Animatable(selectedIndex * segWidth) } + LaunchedEffect(selectedIndex, segWidth) { + indicatorOffset.animateTo(selectedIndex * segWidth, spring(0.8f, 600f)) + } + + // Container row with glass backdrop + Row( + Modifier + .layerBackdrop(trackLayer) + .drawBackdrop( + backdrop = outerBackdrop, + shape = { Capsule() }, + effects = { + vibrancy() + blur(8f.dp.toPx()) + lens(8f.dp.toPx(), 16f.dp.toPx()) + }, + onDrawSurface = { drawRect(containerColor) }, ) + .height(40.dp) + .fillMaxWidth() + .padding(3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + segments.forEach { segment -> + val isSelected = segment.key == selectedKey + Box( + Modifier + .weight(1f) + .clickable( + interactionSource = null, + indication = null, + enabled = enabled, + role = Role.Button, + onClick = { onSelected(segment.key) }, + ), + contentAlignment = Alignment.Center, + ) { + val contentColor = if (isSelected) style.selectedContentColor else style.contentColor + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + ) { + segment.icon?.vectorFor(isSelected)?.let { vector -> + Image( + painter = rememberVectorPainter(vector), + contentDescription = segment.icon.contentDescription, + modifier = Modifier.size(14.dp), + colorFilter = ColorFilter.tint(contentColor), + ) + } + BasicText( + text = segment.label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + color = contentColor, + fontSize = 13.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, + ), + ) + } + } } } + + // Floating glass indicator (like LiquidBottomTabs selected tab) + Box( + Modifier + .graphicsLayer { translationX = indicatorOffset.value } + .drawBackdrop( + backdrop = rememberCombinedBackdrop(outerBackdrop, trackLayer), + shape = { Capsule() }, + effects = { + lens(6f.dp.toPx(), 12f.dp.toPx(), chromaticAberration = true) + }, + highlight = { Highlight.Default }, + shadow = { Shadow(alpha = 0.15f) }, + innerShadow = { InnerShadow(radius = 4f.dp, alpha = 0.08f) }, + onDrawSurface = { + val surfaceColor = if (isLight) Color.Black.copy(0.06f) else Color.White.copy(0.06f) + drawRect(surfaceColor) + }, + ) + .height(34.dp) + .fillMaxWidth(1f / segCount), + ) } } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.android.kt index 9d18407..19a9977 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.android.kt @@ -1,9 +1,14 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + package io.github.androidpoet.liquidkit.slider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import com.kyant.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidSlider @@ -16,9 +21,9 @@ internal actual fun PlatformLiquidSlider( enabled: Boolean, style: LiquidGlassStyle, ) { - val backdrop = rememberCanvasBackdrop { - drawRect(style.containerColor) - } + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop LiquidSlider( value = { value }, @@ -27,9 +32,5 @@ internal actual fun PlatformLiquidSlider( visibilityThreshold = (valueRange.endInclusive - valueRange.start) / 1_000f, backdrop = backdrop, modifier = modifier.alpha(if (enabled) 1f else 0.42f), - enabled = enabled, - activeTrackColor = style.selectedContentColor.copy(alpha = 0.58f), - inactiveTrackColor = style.contentColor.copy(alpha = 0.16f), - thumbColor = style.containerColor.copy(alpha = 0.92f), ) } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.android.kt index 1353374..702eafe 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.android.kt @@ -1,9 +1,14 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + package io.github.androidpoet.liquidkit.toggle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import com.kyant.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidToggle import io.github.androidpoet.liquidkit.LiquidGlassStyle @@ -15,15 +20,14 @@ internal actual fun PlatformLiquidToggle( enabled: Boolean, style: LiquidGlassStyle, ) { - val backdrop = rememberCanvasBackdrop { - drawRect(style.containerColor) - } + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop LiquidToggle( selected = { checked }, onSelect = { if (enabled) onCheckedChange(it) }, backdrop = backdrop, modifier = modifier.alpha(if (enabled) 1f else 0.42f), - enabled = enabled, ) } diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyle.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyle.kt index 06af915..a8848cb 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyle.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyle.kt @@ -14,6 +14,9 @@ public data class LiquidGlassStyle( public val selectedContentColor: Color, ) { public companion object { + + // ── Light mode ────────────────────────────────────────────────────── + public val Surface: LiquidGlassStyle = LiquidGlassStyle( cornerRadius = 24.dp, containerColor = Color.White.copy(alpha = 0.28f), @@ -37,5 +40,31 @@ public data class LiquidGlassStyle( contentColor = Color(0xFF4E5968), selectedContentColor = Color(0xFF101418), ) + + // ── Dark mode ─────────────────────────────────────────────────────── + + public val SurfaceDark: LiquidGlassStyle = LiquidGlassStyle( + cornerRadius = 24.dp, + containerColor = Color.White.copy(alpha = 0.10f), + selectedContainerColor = Color.White.copy(alpha = 0.18f), + contentColor = Color.White.copy(alpha = 0.55f), + selectedContentColor = Color.White.copy(alpha = 0.92f), + ) + + public val ControlDark: LiquidGlassStyle = LiquidGlassStyle( + cornerRadius = 18.dp, + containerColor = Color.White.copy(alpha = 0.12f), + selectedContainerColor = Color.White.copy(alpha = 0.22f), + contentColor = Color.White.copy(alpha = 0.55f), + selectedContentColor = Color.White.copy(alpha = 0.92f), + ) + + public val NavigationBarDark: LiquidGlassStyle = LiquidGlassStyle( + cornerRadius = 28.dp, + containerColor = Color.White.copy(alpha = 0.10f), + selectedContainerColor = Color.White.copy(alpha = 0.18f), + contentColor = Color.White.copy(alpha = 0.50f), + selectedContentColor = Color.White.copy(alpha = 0.92f), + ) } } diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidKitTheme.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidKitTheme.kt new file mode 100644 index 0000000..8dec4ed --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidKitTheme.kt @@ -0,0 +1,36 @@ +package io.github.androidpoet.liquidkit + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf + +/** + * Provides a [LiquidGlassStyle] down the composition so every glass component + * can pick it up without an explicit parameter at each call site. + * + * Defaults to [LiquidGlassStyle.Control] (light). Wrap your app or screen + * in [LiquidKitTheme] to switch to the dark variant automatically. + */ +public val LocalLiquidGlassStyle: androidx.compose.runtime.ProvidableCompositionLocal = + staticCompositionLocalOf { LiquidGlassStyle.Control } + +/** + * Sets the default [LiquidGlassStyle] for all glass components in [content]. + * + * When [style] is omitted the theme auto-selects the appropriate light/dark + * preset based on [isSystemInDarkTheme]. + * + * @param style Override the resolved style. Pass a custom [liquidGlassStyle] + * or one of the [LiquidGlassStyle.Control] / [LiquidGlassStyle.ControlDark] + * etc. presets. + */ +@Composable +public fun LiquidKitTheme( + style: LiquidGlassStyle = if (isSystemInDarkTheme()) LiquidGlassStyle.ControlDark else LiquidGlassStyle.Control, + content: @Composable () -> Unit, +) { + CompositionLocalProvider(LocalLiquidGlassStyle provides style) { + content() + } +} diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/icon/LiquidIcon.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/icon/LiquidIcon.kt index 9f34f47..9235b92 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/icon/LiquidIcon.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/icon/LiquidIcon.kt @@ -5,13 +5,15 @@ import androidx.compose.ui.graphics.vector.ImageVector @Immutable public data class LiquidIcon( - public val imageVector: ImageVector, - public val selectedImageVector: ImageVector = imageVector, + /** Compose vector used on Android and desktop. Null if this icon is iOS-only. */ + public val imageVector: ImageVector? = null, + public val selectedImageVector: ImageVector? = imageVector, public val contentDescription: String? = null, + /** SF Symbol name used on iOS. Null if this icon is Android-only. */ public val iosSystemName: String? = null, public val selectedIosSystemName: String? = iosSystemName, ) { - public fun vectorFor(selected: Boolean): ImageVector = + public fun vectorFor(selected: Boolean): ImageVector? = if (selected) selectedImageVector else imageVector public fun iosSystemNameFor(selected: Boolean): String? = diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/internal/InternalLiquidKitApi.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/internal/InternalLiquidKitApi.kt new file mode 100644 index 0000000..57e8227 --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/internal/InternalLiquidKitApi.kt @@ -0,0 +1,13 @@ +package io.github.androidpoet.liquidkit.internal + +/** + * Marks declarations that are public only to allow cross-module access within + * the LiquidKit library itself. Do not use from application code — binary + * compatibility for these symbols is not guaranteed. + */ +@RequiresOptIn( + message = "This is an internal LiquidKit API. It may change or be removed without notice.", + level = RequiresOptIn.Level.ERROR, +) +@Retention(AnnotationRetention.BINARY) +public annotation class InternalLiquidKitApi diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.kt new file mode 100644 index 0000000..713414b --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.kt @@ -0,0 +1,21 @@ +package io.github.androidpoet.liquidkit.internal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier + +/** + * Holds the platform capture layer used by glass components to sample from the + * actual content drawn behind them. On Android this is a [LayerBackdrop]; on iOS + * it is null (native system handles blur). + */ +@InternalLiquidKitApi +public val LocalLiquidLayerBackdrop: androidx.compose.runtime.ProvidableCompositionLocal = + staticCompositionLocalOf { null } + +@InternalLiquidKitApi +@Composable +public expect fun rememberLiquidLayerBackdrop(): Any? + +@InternalLiquidKitApi +public expect fun Modifier.captureToLiquidLayerBackdrop(backdrop: Any?): Modifier diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidNavigationItem.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidNavigationItem.kt index 1dd8bc9..5f347ca 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidNavigationItem.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidNavigationItem.kt @@ -9,4 +9,9 @@ public data class LiquidNavigationItem( public val label: String, public val icon: LiquidIcon? = null, public val contentDescription: String = label, + /** + * Optional badge. `null` = no badge. `0` = dot indicator. `> 0` = numeric count. + * Capped at 99 in platform renderers. + */ + public val badge: Int? = null, ) diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidTabScaffold.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidTabScaffold.kt index 1d17194..238d374 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidTabScaffold.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidTabScaffold.kt @@ -3,6 +3,7 @@ package io.github.androidpoet.liquidkit.navigation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -11,6 +12,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.captureToLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.rememberLiquidLayerBackdrop @Stable public class LiquidTabState internal constructor(initialKey: T) { @@ -36,6 +41,18 @@ public fun LiquidTabScaffold( navigationModifier: Modifier = Modifier, style: LiquidGlassStyle = LiquidGlassStyle.NavigationBar, onSelected: (T) -> Unit = {}, + bottomNavigation: @Composable BoxScope.( + selectedKey: T, + onSelected: (T) -> Unit, + ) -> Unit = { selectedKey, onTabSelected -> + LiquidBottomNavigation( + items = items, + selectedKey = selectedKey, + onSelected = onTabSelected, + modifier = navigationModifier.align(Alignment.BottomCenter), + style = style, + ) + }, content: @Composable BoxScope.(selectedKey: T) -> Unit, ) { require(items.isNotEmpty()) { "LiquidTabScaffold requires at least one item." } @@ -49,10 +66,12 @@ public fun LiquidTabScaffold( navigationModifier = navigationModifier, style = style, onSelected = onSelected, + bottomNavigation = bottomNavigation, content = content, ) } +@OptIn(InternalLiquidKitApi::class) @Composable public fun LiquidTabScaffold( items: List>, @@ -61,21 +80,35 @@ public fun LiquidTabScaffold( navigationModifier: Modifier = Modifier, style: LiquidGlassStyle = LiquidGlassStyle.NavigationBar, onSelected: (T) -> Unit = {}, + bottomNavigation: @Composable BoxScope.( + selectedKey: T, + onSelected: (T) -> Unit, + ) -> Unit = { selectedKey, onTabSelected -> + LiquidBottomNavigation( + items = items, + selectedKey = selectedKey, + onSelected = onTabSelected, + modifier = navigationModifier.align(Alignment.BottomCenter), + style = style, + ) + }, content: @Composable BoxScope.(selectedKey: T) -> Unit, ) { require(items.isNotEmpty()) { "LiquidTabScaffold requires at least one item." } + val layerBackdrop = rememberLiquidLayerBackdrop() Box(modifier = modifier) { - content(state.selectedKey) - LiquidBottomNavigation( - items = items, - selectedKey = state.selectedKey, - onSelected = { key -> + // Content records into the layer — must NOT read from it or a circular GraphicsLayer + // reference forms (content-capture layer → toggle inside content → same layer → ∞). + Box(Modifier.matchParentSize().captureToLiquidLayerBackdrop(layerBackdrop)) { + content(state.selectedKey) + } + // Nav bar is scoped to the layer so it can blur the content behind it. + CompositionLocalProvider(LocalLiquidLayerBackdrop provides layerBackdrop) { + bottomNavigation(state.selectedKey) { key -> state.select(key) onSelected(key) - }, - modifier = navigationModifier.align(Alignment.BottomCenter), - style = style, - ) + } + } } } diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.kt index b3e0e37..8ac934d 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.kt @@ -3,10 +3,14 @@ package io.github.androidpoet.liquidkit.segmented import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle +import io.github.androidpoet.liquidkit.icon.LiquidIcon public data class LiquidSegment( public val key: T, public val label: String, + /** Optional icon shown alongside the label. Platform renderers display it when supported. */ + public val icon: LiquidIcon? = null, ) @Composable @@ -16,7 +20,7 @@ public fun LiquidSegmentedControl( onSelected: (T) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - style: LiquidGlassStyle = LiquidGlassStyle.Control, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, ) { require(segments.isNotEmpty()) { "LiquidSegmentedControl requires at least one segment." } diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.kt index acae8a8..3932fa8 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.kt @@ -3,6 +3,7 @@ package io.github.androidpoet.liquidkit.slider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle @Composable public fun LiquidSlider( @@ -11,7 +12,7 @@ public fun LiquidSlider( modifier: Modifier = Modifier, valueRange: ClosedFloatingPointRange = 0f..1f, enabled: Boolean = true, - style: LiquidGlassStyle = LiquidGlassStyle.Control, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, ) { require(valueRange.start < valueRange.endInclusive) { "LiquidSlider requires a non-empty valueRange." diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.kt index 85a522a..94ec84f 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.kt @@ -3,6 +3,7 @@ package io.github.androidpoet.liquidkit.toggle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle @Composable public fun LiquidToggle( @@ -10,7 +11,7 @@ public fun LiquidToggle( onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - style: LiquidGlassStyle = LiquidGlassStyle.Control, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, ) { PlatformLiquidToggle( checked = checked, diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.ios.kt new file mode 100644 index 0000000..2098acb --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/LiquidGlassBackdropBridge.ios.kt @@ -0,0 +1,9 @@ +package io.github.androidpoet.liquidkit.internal + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public actual fun rememberLiquidLayerBackdrop(): Any? = null + +public actual fun Modifier.captureToLiquidLayerBackdrop(backdrop: Any?): Modifier = this diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.ios.kt index 75be4a7..bf8c586 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.ios.kt @@ -84,6 +84,11 @@ private fun UITabBar.configureLiquidTabBar( selectedImage = image, ).apply { tag = index.toLong() + badgeValue = when (item.badge) { + null -> null + 0 -> "" + else -> item.badge.coerceAtMost(99).toString() + } } } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.ios.kt index 163bdf8..90a6b57 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.ios.kt @@ -20,6 +20,7 @@ import platform.UIKit.NSForegroundColorAttributeName import platform.UIKit.UIColor import platform.UIKit.UIControlEventValueChanged import platform.UIKit.UIControlStateNormal +import platform.UIKit.UIImage import platform.UIKit.UIControlStateSelected import platform.UIKit.UISegmentedControl import platform.darwin.NSObject @@ -86,7 +87,17 @@ private fun UISegmentedControl.configureLiquidSegmentedControl( ) { removeAllSegments() segments.forEachIndexed { index, segment -> - insertSegmentWithTitle(segment.label, atIndex = index.toULong(), animated = false) + val systemImageName = segment.icon?.iosSystemName + if (systemImageName != null) { + val image = UIImage.systemImageNamed(systemImageName) + if (image != null) { + insertSegmentWithImage(image, atIndex = index.toULong(), animated = false) + } else { + insertSegmentWithTitle(segment.label, atIndex = index.toULong(), animated = false) + } + } else { + insertSegmentWithTitle(segment.label, atIndex = index.toULong(), animated = false) + } setEnabled(enabled, forSegmentAtIndex = index.toULong()) } selectedSegmentIndex = segments.indexOfFirst { it.key == selectedKey }.coerceAtLeast(0).toLong() diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt index 81a9b13..fef22a6 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt @@ -49,6 +49,7 @@ internal actual fun PlatformLiquidToggle( UISwitch().apply { setOn(checked) this.enabled = enabled + onTintColor = style.selectedContentColor.toUIColor() backgroundColor = UIColor.clearColor setOpaque(false) addTarget( @@ -64,6 +65,7 @@ internal actual fun PlatformLiquidToggle( uiSwitch.setOn(checked, animated = true) } uiSwitch.enabled = enabled + uiSwitch.onTintColor = style.selectedContentColor.toUIColor() }, onRelease = {}, properties = UIKitInteropProperties( diff --git a/sampleApp/src/androidMain/kotlin/com/kyant/backdrop/catalog/MainActivity.kt b/sampleApp/src/androidMain/kotlin/com/kyant/backdrop/catalog/MainActivity.kt deleted file mode 100644 index 989d48a..0000000 --- a/sampleApp/src/androidMain/kotlin/com/kyant/backdrop/catalog/MainActivity.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.kyant.backdrop.catalog - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.core.view.WindowCompat -import io.github.androidpoet.liquidkit.sample.LiquidKitComposeOwnedTabShell - -class MainActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) - - setContent { - LiquidKitComposeOwnedTabShell() - } - } -} diff --git a/sampleApp/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sample/MainActivity.kt b/sampleApp/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sample/MainActivity.kt new file mode 100644 index 0000000..fae91bd --- /dev/null +++ b/sampleApp/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sample/MainActivity.kt @@ -0,0 +1,306 @@ +package io.github.androidpoet.liquidkit.sample + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.paint +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.view.WindowCompat +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidBottomTab +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidBottomTabs +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidButton +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidSlider +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidToggle + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { GlassCatalogApp() } + } +} + +private enum class Screen { Home, Buttons, Controls, BottomTabs } + +@Composable +private fun GlassCatalogApp() { + var screen by rememberSaveable { mutableStateOf(Screen.Home) } + BackHandler(screen != Screen.Home) { screen = Screen.Home } + + when (screen) { + Screen.Home -> HomeScreen(onNavigate = { screen = it }) + Screen.Buttons -> ButtonsScreen() + Screen.Controls -> ControlsScreen() + Screen.BottomTabs -> BottomTabsScreen() + } +} + +// ── Scaffold ───────────────────────────────────────────────────────────────── + +@Composable +private fun DemoScaffold( + wallpaper: Int = R.drawable.wallpaper_light, + content: @Composable BoxScope.(backdrop: LayerBackdrop) -> Unit +) { + val backdrop = rememberLayerBackdrop() + + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Image( + painter = painterResource(wallpaper), + contentDescription = null, + modifier = Modifier + .layerBackdrop(backdrop) + .fillMaxSize(), + contentScale = ContentScale.Crop, + ) + content(backdrop) + } +} + +// ── Home ────────────────────────────────────────────────────────────────────── + +@Composable +private fun HomeScreen(onNavigate: (Screen) -> Unit) { + val isLight = !isSystemInDarkTheme() + val fg = if (isLight) Color.Black else Color.White + + DemoScaffold { + Column( + Modifier + .verticalScroll(rememberScrollState()) + .systemBarsPadding() + .displayCutoutPadding() + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(0.dp), + ) { + BasicText( + "Glass Catalog", + Modifier.padding(16.dp, 40.dp, 16.dp, 8.dp), + style = TextStyle(fg, 28.sp, FontWeight.SemiBold), + ) + BasicText( + "Kyant-style liquid glass components on Android", + Modifier.padding(horizontal = 16.dp).padding(bottom = 24.dp), + style = TextStyle(fg.copy(0.5f), 14.sp), + ) + + SectionHeader("Components", fg) + NavRow("Buttons", fg) { onNavigate(Screen.Buttons) } + NavRow("Toggle & Slider", fg) { onNavigate(Screen.Controls) } + NavRow("Bottom tabs", fg) { onNavigate(Screen.BottomTabs) } + } + } +} + +@Composable +private fun SectionHeader(label: String, fg: Color) { + BasicText( + label, + Modifier.padding(16.dp, 20.dp, 16.dp, 6.dp).fillMaxWidth(), + style = TextStyle(Color(0xFF0088FF), 13.sp, FontWeight.SemiBold), + ) +} + +@Composable +private fun NavRow(label: String, fg: Color, onClick: () -> Unit) { + Row( + Modifier + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 14.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + BasicText(label, style = TextStyle(fg, 17.sp)) + BasicText("›", style = TextStyle(fg.copy(0.4f), 20.sp)) + } + Box(Modifier.padding(horizontal = 16.dp).fillMaxWidth().height(0.5.dp).background(fg.copy(0.08f))) +} + +// ── Buttons ─────────────────────────────────────────────────────────────────── + +@Composable +private fun ButtonsScreen() { + DemoScaffold { backdrop -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + LiquidButton({}, backdrop) { + BasicText("Transparent Liquid Button", style = TextStyle(Color.Black, 15.sp)) + } + LiquidButton({}, backdrop, surfaceColor = Color.White.copy(0.3f)) { + BasicText("Surface Liquid Button", style = TextStyle(Color.Black, 15.sp)) + } + LiquidButton({}, backdrop, tint = Color(0xFF0088FF)) { + BasicText("Blue Tinted Button", style = TextStyle(Color.White, 15.sp)) + } + LiquidButton({}, backdrop, tint = Color(0xFFFF8D28)) { + BasicText("Orange Tinted Button", style = TextStyle(Color.White, 15.sp)) + } + LiquidButton({}, backdrop, tint = Color(0xFF34C759)) { + BasicText("Green Tinted Button", style = TextStyle(Color.White, 15.sp)) + } + } + } +} + +// ── Controls ────────────────────────────────────────────────────────────────── + +@Composable +private fun ControlsScreen() { + val isLight = !isSystemInDarkTheme() + val cardBg = if (isLight) Color.White else Color(0xFF1C1C1E) + + DemoScaffold { backdrop -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp), + modifier = Modifier.padding(horizontal = 32.dp), + ) { + // Toggle directly over wallpaper + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + BasicText("Toggle", style = TextStyle(Color.White, 13.sp, FontWeight.Medium)) + var tog1 by rememberSaveable { mutableStateOf(false) } + LiquidToggle({ tog1 }, { tog1 = it }, backdrop) + + // Toggle over card (canvas backdrop) + var tog2 by rememberSaveable { mutableStateOf(true) } + Box(Modifier.background(cardBg, androidx.compose.foundation.shape.RoundedCornerShape(20.dp)).padding(20.dp)) { + LiquidToggle({ tog2 }, { tog2 = it }, rememberCanvasBackdrop { drawRect(cardBg) }) + } + } + + // Slider + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { + BasicText("Slider", style = TextStyle(Color.White, 13.sp, FontWeight.Medium)) + var value by rememberSaveable { mutableStateOf(0.42f) } + LiquidSlider( + value = { value }, + onValueChange = { value = it }, + valueRange = 0f..1f, + visibilityThreshold = 0.001f, + backdrop = backdrop, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +// ── Bottom Tabs ─────────────────────────────────────────────────────────────── + +@Composable +private fun BottomTabsScreen() { + val isLight = !isSystemInDarkTheme() + val fg = if (isLight) Color.Black else Color.White + val icon = painterResource(R.drawable.flight_40px) + val iconFilter = ColorFilter.tint(fg) + + DemoScaffold(wallpaper = R.drawable.system_home_screen_light) { backdrop -> + Column( + Modifier.navigationBarsPadding().fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(40.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // 3 tabs + var tab3 by rememberSaveable { mutableIntStateOf(0) } + LiquidBottomTabs( + selectedTabIndex = { tab3 }, + onTabSelected = { tab3 = it }, + backdrop = backdrop, + tabsCount = 3, + modifier = Modifier.padding(horizontal = 36.dp), + ) { + repeat(3) { i -> + LiquidBottomTab({ tab3 = i }) { + Box(Modifier.size(28.dp).paint(icon, colorFilter = iconFilter)) + BasicText("Tab ${i + 1}", style = TextStyle(fg, 12.sp)) + } + } + } + + // 4 tabs + var tab4 by rememberSaveable { mutableIntStateOf(1) } + LiquidBottomTabs( + selectedTabIndex = { tab4 }, + onTabSelected = { tab4 = it }, + backdrop = backdrop, + tabsCount = 4, + modifier = Modifier.padding(horizontal = 24.dp), + ) { + repeat(4) { i -> + LiquidBottomTab({ tab4 = i }) { + Box(Modifier.size(28.dp).paint(icon, colorFilter = iconFilter)) + BasicText("Tab ${i + 1}", style = TextStyle(fg, 12.sp)) + } + } + } + + // 5 tabs + var tab5 by rememberSaveable { mutableIntStateOf(2) } + LiquidBottomTabs( + selectedTabIndex = { tab5 }, + onTabSelected = { tab5 = it }, + backdrop = backdrop, + tabsCount = 5, + modifier = Modifier.padding(horizontal = 8.dp), + ) { + repeat(5) { i -> + LiquidBottomTab({ tab5 = i }) { + Box(Modifier.size(24.dp).paint(icon, colorFilter = iconFilter)) + BasicText("${i + 1}", style = TextStyle(fg, 11.sp)) + } + } + } + } + } +} diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt index 26f1cf9..60ebfbd 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt @@ -96,12 +96,6 @@ public fun LiquidKitSampleTabContent( modifier: Modifier = Modifier, extraContent: @Composable ColumnScope.() -> Unit = {}, ) { - var notificationsEnabled by remember { mutableStateOf(true) } - var compactModeEnabled by remember { mutableStateOf(false) } - var intensity by remember { mutableStateOf(0.58f) } - var density by remember { mutableStateOf(ControlDensity.Regular) } - val densitySegments = rememberControlDensitySegments() - Column( modifier = modifier .fillMaxSize() @@ -118,39 +112,10 @@ public fun LiquidKitSampleTabContent( .widthIn(max = 620.dp), ) - ComponentPanel( - modifier = Modifier - .fillMaxWidth() - .widthIn(max = 620.dp), - ) { - LiquidKitToggleRow( - label = "LiquidKit Toggle", - description = "Rendered by AndroidLiquidGlass on Android and UISwitch on iOS.", - value = notificationsEnabled, - onValueChange = { notificationsEnabled = it }, - ) - PanelDivider() - LiquidKitToggleRow( - label = "LiquidKit Compact Toggle", - description = "Same common API, platform-native renderer underneath.", - value = compactModeEnabled, - onValueChange = { compactModeEnabled = it }, - ) - PanelDivider() - LiquidKitSliderRow( - label = "LiquidKit Slider", - description = "Android uses AndroidLiquidGlass slider; iOS uses UISlider.", - value = intensity, - onValueChange = { intensity = it }, - ) - PanelDivider() - LiquidKitSegmentedRow( - label = "LiquidKit Segmented Control", - description = "Compact segmented choices with native UISegmentedControl on iOS.", - segments = densitySegments, - selected = density, - onSelected = { density = it }, - ) + when (selectedTab) { + LiquidKitSampleTab.Home -> HomeSamplePanel() + LiquidKitSampleTab.Search -> ControlsSamplePanel() + LiquidKitSampleTab.Settings -> SettingsSamplePanel() } extraContent() @@ -172,6 +137,152 @@ public fun LiquidKitSampleTabRoot( ) } +@Composable +private fun HomeSamplePanel() { + var enabled by remember { mutableStateOf(true) } + var intensity by remember { mutableStateOf(0.58f) } + + InvertedPanel( + modifier = Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), + ) { + BasicText( + text = "One API. Native ownership.", + style = TextStyle( + color = Color.White, + fontSize = 23.sp, + fontWeight = FontWeight.Bold, + lineHeight = 28.sp, + ), + ) + BasicText( + text = "Android renders Kyant-style liquid controls. iOS keeps native tab ownership.", + style = TextStyle( + color = Color(0xFFD7D7D7), + fontSize = 14.sp, + lineHeight = 20.sp, + ), + ) + Spacer(modifier = Modifier.height(10.dp)) + InvertedMetricRow(label = "Tab layer", value = "Nav3") + InvertedMetricRow(label = "Android renderer", value = "LiquidGlass") + } + + ComponentPanel( + modifier = Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), + ) { + PanelSectionTitle( + title = "Home controls", + description = "A simple monochrome surface for checking the shared API.", + ) + PanelDivider() + LiquidKitToggleRow( + label = "Liquid surface", + description = "Toggle the sample state without changing the tab renderer.", + value = enabled, + onValueChange = { enabled = it }, + ) + PanelDivider() + LiquidKitSliderRow( + label = "Glass intensity", + description = "Tune the shared control state used by this preview.", + value = intensity, + onValueChange = { intensity = it }, + ) + PanelDivider() + StatusRow(label = "Renderer", value = "AndroidLiquidGlass") + PanelDivider() + StatusRow(label = "Navigation", value = "Compose Nav3 tabs") + } +} + +@Composable +private fun ControlsSamplePanel() { + var density by remember { mutableStateOf(ControlDensity.Regular) } + var compactModeEnabled by remember { mutableStateOf(false) } + var intensity by remember { mutableStateOf(0.42f) } + val densitySegments = rememberControlDensitySegments() + + ComponentPanel( + modifier = Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), + ) { + PanelSectionTitle( + title = "Controls lab", + description = "A clean black and white workbench for component states.", + ) + PanelDivider() + LiquidKitSegmentedRow( + label = "Density", + description = "Switch layout density for app-level controls.", + segments = densitySegments, + selected = density, + onSelected = { density = it }, + ) + PanelDivider() + LiquidKitToggleRow( + label = "Compact controls", + description = "Keep touch targets stable while reducing visual weight.", + value = compactModeEnabled, + onValueChange = { compactModeEnabled = it }, + ) + PanelDivider() + LiquidKitSliderRow( + label = "Response", + description = "Exercise continuous input on the Android liquid slider.", + value = intensity, + onValueChange = { intensity = it }, + ) + } +} + +@Composable +private fun SettingsSamplePanel() { + var nativeTabs by remember { mutableStateOf(true) } + var reducedMotion by remember { mutableStateOf(false) } + var density by remember { mutableStateOf(ControlDensity.Regular) } + val densitySegments = rememberControlDensitySegments() + + ComponentPanel( + modifier = Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), + ) { + PanelSectionTitle( + title = "Library settings", + description = "A focused settings surface for platform behavior.", + ) + PanelDivider() + LiquidKitToggleRow( + label = "Native tabs on iOS", + description = "Keep iOS tab ownership in SwiftUI and Android tabs in Compose.", + value = nativeTabs, + onValueChange = { nativeTabs = it }, + ) + PanelDivider() + LiquidKitToggleRow( + label = "Reduced motion", + description = "Preview a quieter configuration for motion-sensitive users.", + value = reducedMotion, + onValueChange = { reducedMotion = it }, + ) + PanelDivider() + LiquidKitSegmentedRow( + label = "Default density", + description = "Choose the default control spacing for host apps.", + segments = densitySegments, + selected = density, + onSelected = { density = it }, + ) + PanelDivider() + StatusRow(label = "Package", value = "LiquidKit") + } +} + @Serializable private data object LiquidKitHomeRoute : NavKey @@ -232,10 +343,15 @@ private fun Header( selectedTab: LiquidKitSampleTab, modifier: Modifier = Modifier, ) { + val title = when (selectedTab) { + LiquidKitSampleTab.Home -> "LiquidKit Home" + LiquidKitSampleTab.Search -> "Controls Lab" + LiquidKitSampleTab.Settings -> "Settings" + } val subtitle = when (selectedTab) { - LiquidKitSampleTab.Home -> "Bottom navigation, toggle, slider, and segmented controls." - LiquidKitSampleTab.Search -> "Android uses vendored AndroidLiquidGlass controls." - LiquidKitSampleTab.Settings -> "iOS 26 uses native SwiftUI TabView while Compose renders tab content." + LiquidKitSampleTab.Home -> "Bottom navigation with a single moving liquid selection." + LiquidKitSampleTab.Search -> "Android controls rendered by vendored AndroidLiquidGlass." + LiquidKitSampleTab.Settings -> "Platform ownership stays simple: SwiftUI tabs on iOS, Compose tabs on Android." } Column( @@ -243,9 +359,9 @@ private fun Header( verticalArrangement = Arrangement.spacedBy(8.dp), ) { BasicText( - text = "LiquidKit", + text = title, style = TextStyle( - color = Color(0xFF0F1720), + color = Color(0xFF070707), fontSize = 34.sp, fontWeight = FontWeight.Bold, lineHeight = 38.sp, @@ -254,7 +370,7 @@ private fun Header( BasicText( text = subtitle, style = TextStyle( - color = Color(0xFF4A5868), + color = Color(0xFF4A4A4A), fontSize = 15.sp, lineHeight = 21.sp, ), @@ -262,6 +378,69 @@ private fun Header( } } +@Composable +private fun InvertedPanel( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Column( + modifier = modifier + .background( + color = Color(0xFF080808), + shape = RoundedCornerShape(32.dp), + ) + .border( + width = 1.dp, + color = Color(0xFF2A2A2A), + shape = RoundedCornerShape(32.dp), + ) + .padding(22.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + content = content, + ) +} + +@Composable +private fun InvertedMetricRow( + label: String, + value: String, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicText( + text = label, + modifier = Modifier.weight(1f), + style = TextStyle( + color = Color(0xFFAFAFAF), + fontSize = 14.sp, + ), + ) + BasicText( + text = value, + style = TextStyle( + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + } +} + +@Composable +private fun PanelSectionTitle( + title: String, + description: String, +) { + LabelBlock( + label = title, + description = description, + modifier = Modifier.padding(vertical = 16.dp), + ) +} + @Composable private fun ComponentPanel( modifier: Modifier = Modifier, @@ -270,12 +449,12 @@ private fun ComponentPanel( Column( modifier = modifier .background( - color = Color(0xCCFFFFFF), + color = Color(0xF7FFFFFF), shape = RoundedCornerShape(28.dp), ) .border( width = 1.dp, - color = Color(0x80FFFFFF), + color = Color(0x1F000000), shape = RoundedCornerShape(28.dp), ) .padding(horizontal = 18.dp, vertical = 8.dp), @@ -283,13 +462,45 @@ private fun ComponentPanel( ) } +@Composable +private fun StatusRow( + label: String, + value: String, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicText( + text = label, + modifier = Modifier.weight(1f), + style = TextStyle( + color = Color(0xFF505050), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ), + ) + BasicText( + text = value, + style = TextStyle( + color = Color(0xFF070707), + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + } +} + @Composable private fun PanelDivider() { Spacer( modifier = Modifier .fillMaxWidth() .height(1.dp) - .background(Color(0x1F4A5868)), + .background(Color(0x1F000000)), ) } @@ -365,7 +576,7 @@ private fun LiquidKitSliderRow( BasicText( text = "${(value * 100).toInt()}%", style = TextStyle( - color = Color(0xFF111820), + color = Color(0xFF070707), fontSize = 14.sp, fontWeight = FontWeight.SemiBold, ), @@ -419,7 +630,7 @@ private fun LabelBlock( BasicText( text = label, style = TextStyle( - color = Color(0xFF111820), + color = Color(0xFF070707), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ), @@ -427,7 +638,7 @@ private fun LabelBlock( BasicText( text = description, style = TextStyle( - color = Color(0xFF43515F), + color = Color(0xFF555555), fontSize = 13.sp, lineHeight = 18.sp, ), @@ -437,8 +648,8 @@ private fun LabelBlock( private fun sampleBackground(): Brush = Brush.linearGradient( colors = listOf( - Color(0xFFEAF5F7), - Color(0xFFF6ECE5), - Color(0xFFE9EDF8), + Color(0xFFF7F7F7), + Color(0xFFEDEDED), + Color(0xFFDADADA), ), ) From e1a4119cd63eac15c9f443185acdb312c60e1e19 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 02:40:34 +0530 Subject: [PATCH 02/11] Fix iOS LiquidToggle compile: import toUIColor helper The iOS actual for LiquidToggle referenced Color.toUIColor() without importing it from the internal package, breaking compileKotlinIosSimulatorArm64. Add the missing import so the native UISwitch interop compiles and links. --- .../io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt index fef22a6..694ead0 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode import androidx.compose.ui.viewinterop.UIKitInteropProperties import androidx.compose.ui.viewinterop.UIKitView import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.toUIColor import kotlinx.cinterop.BetaInteropApi import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ObjCAction From 626a76eba9cc96c33e187f0f2a7dc1c85beec3bf Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 02:45:27 +0530 Subject: [PATCH 03/11] Ignore IDE/user files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2dfe1b1..c9bdf66 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ /*/build/ iosApp/DerivedData/ local.properties + +.idea/ +xcuserdata/ From 43e8039ebb7b618cb36ebccbfee4be1ea7bee1d4 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 02:51:11 +0530 Subject: [PATCH 04/11] Add Liquid Glass LiquidButton and LiquidFab components --- .../liquidkit/button/LiquidButton.android.kt | 54 +++++++ .../liquidkit/button/LiquidFab.android.kt | 54 +++++++ .../liquidkit/button/LiquidButton.kt | 58 ++++++++ .../androidpoet/liquidkit/button/LiquidFab.kt | 47 +++++++ .../liquidkit/button/LiquidButton.ios.kt | 78 ++++++++++ .../liquidkit/button/LiquidFab.ios.kt | 86 +++++++++++ .../internal/GlassButtonConfiguration.ios.kt | 18 +++ .../sample/showcase/ButtonShowcase.kt | 133 ++++++++++++++++++ 8 files changed, 528 insertions(+) create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.android.kt create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.android.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.ios.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.ios.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/GlassButtonConfiguration.ios.kt create mode 100644 sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ButtonShowcase.kt diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.android.kt new file mode 100644 index 0000000..7b034bb --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.android.kt @@ -0,0 +1,54 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.button + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidButton as EngineLiquidButton + +@Composable +internal actual fun PlatformLiquidButton( + text: String, + onClick: () -> Unit, + modifier: Modifier, + variant: LiquidButtonVariant, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + val prominent = variant == LiquidButtonVariant.GlassProminent + val labelColor = if (prominent) style.selectedContentColor else style.contentColor + + EngineLiquidButton( + onClick = { if (enabled) onClick() }, + backdrop = backdrop, + isInteractive = enabled, + surfaceColor = if (prominent) style.selectedContainerColor else androidx.compose.ui.graphics.Color.Unspecified, + modifier = modifier.alpha(if (enabled) 1f else 0.42f), + ) { + BasicText( + text = text, + modifier = Modifier.padding(horizontal = 4.dp), + style = TextStyle( + color = labelColor, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + } +} diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.android.kt new file mode 100644 index 0000000..bd096c0 --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.android.kt @@ -0,0 +1,54 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.button + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.icon.LiquidIcon +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidButton as EngineLiquidButton + +@Composable +internal actual fun PlatformLiquidFab( + icon: LiquidIcon, + onClick: () -> Unit, + modifier: Modifier, + variant: LiquidButtonVariant, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + val prominent = variant == LiquidButtonVariant.GlassProminent + val iconTint = if (prominent) style.selectedContentColor else style.contentColor + + EngineLiquidButton( + onClick = { if (enabled) onClick() }, + backdrop = backdrop, + isInteractive = enabled, + surfaceColor = if (prominent) style.selectedContainerColor else Color.Unspecified, + modifier = modifier.size(56.dp).alpha(if (enabled) 1f else 0.42f), + ) { + icon.vectorFor(true)?.let { vector -> + Image( + painter = rememberVectorPainter(vector), + contentDescription = icon.contentDescription, + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(iconTint), + ) + } + } +} diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.kt new file mode 100644 index 0000000..c338ca1 --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.kt @@ -0,0 +1,58 @@ +package io.github.androidpoet.liquidkit.button + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle + +/** + * Visual variants for [LiquidButton], mirroring the iOS 26 Liquid Glass button styles. + */ +public enum class LiquidButtonVariant { + /** Translucent, secondary-emphasis glass (iOS `UIButton.Configuration.glass()`). */ + Glass, + + /** Opaque, primary-emphasis glass (iOS `UIButton.Configuration.prominentGlass()`). */ + GlassProminent, +} + +/** + * A Liquid Glass button. + * + * On Android it is rendered with the LiquidKit backdrop shader engine; on iOS it is a genuine + * native [platform.UIKit.UIButton] configured with the system Liquid Glass configuration so it + * picks up authentic system rendering. + * + * @param text label shown inside the button. + * @param onClick invoked when the button is tapped. + * @param variant [LiquidButtonVariant.Glass] (secondary/translucent) or + * [LiquidButtonVariant.GlassProminent] (primary/opaque). + */ +@Composable +public fun LiquidButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + variant: LiquidButtonVariant = LiquidButtonVariant.Glass, + enabled: Boolean = true, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, +) { + PlatformLiquidButton( + text = text, + onClick = onClick, + modifier = modifier, + variant = variant, + enabled = enabled, + style = style, + ) +} + +@Composable +internal expect fun PlatformLiquidButton( + text: String, + onClick: () -> Unit, + modifier: Modifier, + variant: LiquidButtonVariant, + enabled: Boolean, + style: LiquidGlassStyle, +) diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.kt new file mode 100644 index 0000000..e1ff75b --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.kt @@ -0,0 +1,47 @@ +package io.github.androidpoet.liquidkit.button + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle +import io.github.androidpoet.liquidkit.icon.LiquidIcon + +/** + * A circular Liquid Glass floating action button. + * + * On Android it is rendered with the LiquidKit backdrop shader engine; on iOS it is a genuine + * native [platform.UIKit.UIButton] configured with the system Liquid Glass configuration. + * + * @param icon symbol shown at the center of the FAB. + * @param onClick invoked when the FAB is tapped. + * @param variant [LiquidButtonVariant.Glass] (translucent) or + * [LiquidButtonVariant.GlassProminent] (opaque, primary). + */ +@Composable +public fun LiquidFab( + icon: LiquidIcon, + onClick: () -> Unit, + modifier: Modifier = Modifier, + variant: LiquidButtonVariant = LiquidButtonVariant.GlassProminent, + enabled: Boolean = true, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, +) { + PlatformLiquidFab( + icon = icon, + onClick = onClick, + modifier = modifier, + variant = variant, + enabled = enabled, + style = style, + ) +} + +@Composable +internal expect fun PlatformLiquidFab( + icon: LiquidIcon, + onClick: () -> Unit, + modifier: Modifier, + variant: LiquidButtonVariant, + enabled: Boolean, + style: LiquidGlassStyle, +) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.ios.kt new file mode 100644 index 0000000..833bb19 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.ios.kt @@ -0,0 +1,78 @@ +package io.github.androidpoet.liquidkit.button + +import androidx.compose.foundation.layout.heightIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.glassButtonConfigurationFor +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCAction +import platform.Foundation.NSSelectorFromString +import platform.UIKit.UIButton +import platform.UIKit.UIColor +import platform.UIKit.UIControlEventTouchUpInside +import platform.UIKit.UIControlStateNormal +import platform.darwin.NSObject + +@OptIn(BetaInteropApi::class) +private class LiquidButtonTarget( + var onClick: () -> Unit, +) : NSObject() { + @ObjCAction + fun tapped() { + onClick() + } +} + +@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidButton( + text: String, + onClick: () -> Unit, + modifier: Modifier, + variant: LiquidButtonVariant, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val currentOnClick = rememberUpdatedState(onClick) + val target = remember { LiquidButtonTarget { currentOnClick.value() } } + target.onClick = { currentOnClick.value() } + + UIKitView( + factory = { + UIButton().apply { + configuration = glassButtonConfigurationFor( + prominent = variant == LiquidButtonVariant.GlassProminent, + ) + setTitle(text, forState = UIControlStateNormal) + this.enabled = enabled + backgroundColor = UIColor.clearColor + setOpaque(false) + addTarget( + target = target, + action = NSSelectorFromString("tapped"), + forControlEvents = UIControlEventTouchUpInside, + ) + } + }, + modifier = modifier.heightIn(min = 50.dp), + update = { button -> + button.setTitle(text, forState = UIControlStateNormal) + button.enabled = enabled + }, + onRelease = {}, + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), + ) +} diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.ios.kt new file mode 100644 index 0000000..0e6f14a --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.ios.kt @@ -0,0 +1,86 @@ +package io.github.androidpoet.liquidkit.button + +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.icon.LiquidIcon +import io.github.androidpoet.liquidkit.internal.glassButtonConfigurationFor +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCAction +import platform.Foundation.NSSelectorFromString +import platform.UIKit.UIButton +import platform.UIKit.UIColor +import platform.UIKit.UIControlEventTouchUpInside +import platform.UIKit.UIControlStateNormal +import platform.UIKit.UIImage +import platform.darwin.NSObject + +@OptIn(BetaInteropApi::class) +private class LiquidFabTarget( + var onClick: () -> Unit, +) : NSObject() { + @ObjCAction + fun tapped() { + onClick() + } +} + +@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidFab( + icon: LiquidIcon, + onClick: () -> Unit, + modifier: Modifier, + variant: LiquidButtonVariant, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val currentOnClick = rememberUpdatedState(onClick) + val target = remember { LiquidFabTarget { currentOnClick.value() } } + target.onClick = { currentOnClick.value() } + + val systemName = icon.iosSystemNameFor(true) + + UIKitView( + factory = { + UIButton().apply { + configuration = glassButtonConfigurationFor( + prominent = variant == LiquidButtonVariant.GlassProminent, + ) + systemName?.let { name -> + setImage(UIImage.systemImageNamed(name), forState = UIControlStateNormal) + } + this.enabled = enabled + backgroundColor = UIColor.clearColor + setOpaque(false) + addTarget( + target = target, + action = NSSelectorFromString("tapped"), + forControlEvents = UIControlEventTouchUpInside, + ) + } + }, + modifier = modifier.requiredSize(56.dp), + update = { button -> + systemName?.let { name -> + button.setImage(UIImage.systemImageNamed(name), forState = UIControlStateNormal) + } + button.enabled = enabled + }, + onRelease = {}, + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), + ) +} diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/GlassButtonConfiguration.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/GlassButtonConfiguration.ios.kt new file mode 100644 index 0000000..d1110d7 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/GlassButtonConfiguration.ios.kt @@ -0,0 +1,18 @@ +package io.github.androidpoet.liquidkit.internal + +import platform.UIKit.UIButtonConfiguration + +/** + * Returns the iOS 26 system Liquid Glass [UIButtonConfiguration] for a button. + * + * @param prominent when true returns the opaque, primary-emphasis prominent glass configuration; + * otherwise the translucent secondary glass configuration. + * + * On a device/SDK that exposes these factories (iOS 26+) this yields authentic system Liquid Glass. + */ +internal fun glassButtonConfigurationFor(prominent: Boolean): UIButtonConfiguration = + if (prominent) { + UIButtonConfiguration.prominentGlassButtonConfiguration() + } else { + UIButtonConfiguration.glassButtonConfiguration() + } diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ButtonShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ButtonShowcase.kt new file mode 100644 index 0000000..ecdaf79 --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ButtonShowcase.kt @@ -0,0 +1,133 @@ +package io.github.androidpoet.liquidkit.sample.showcase + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.button.LiquidButton +import io.github.androidpoet.liquidkit.button.LiquidButtonVariant +import io.github.androidpoet.liquidkit.button.LiquidFab +import io.github.androidpoet.liquidkit.icon.LiquidIcon + +private val PlusIcon: ImageVector by lazy { + ImageVector.Builder( + name = "Plus", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(Color.White)) { + moveTo(11f, 5f) + horizontalLineTo(13f) + verticalLineTo(11f) + horizontalLineTo(19f) + verticalLineTo(13f) + horizontalLineTo(13f) + verticalLineTo(19f) + horizontalLineTo(11f) + verticalLineTo(13f) + horizontalLineTo(5f) + verticalLineTo(11f) + horizontalLineTo(11f) + close() + } + }.build() +} + +/** + * Self-contained showcase for [LiquidButton] (both variants) and [LiquidFab]. + * + * Renders Android shader glass on Android and native UIButton glass on iOS. + */ +@Composable +public fun ButtonShowcase() { + var clicks by remember { mutableStateOf(0) } + + val fabIcon = remember { + LiquidIcon( + imageVector = PlusIcon, + contentDescription = "Add", + iosSystemName = "plus", + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + BasicText( + text = "Liquid Glass Buttons", + style = TextStyle( + color = Color(0xFF101418), + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ), + ) + + BasicText( + text = "Taps: $clicks", + style = TextStyle(color = Color(0xFF4E5968), fontSize = 14.sp), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LiquidButton( + text = "Glass", + onClick = { clicks++ }, + variant = LiquidButtonVariant.Glass, + ) + LiquidButton( + text = "Prominent", + onClick = { clicks++ }, + variant = LiquidButtonVariant.GlassProminent, + ) + } + + LiquidButton( + text = "Disabled", + onClick = { clicks++ }, + variant = LiquidButtonVariant.GlassProminent, + enabled = false, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LiquidFab( + icon = fabIcon, + onClick = { clicks++ }, + variant = LiquidButtonVariant.Glass, + ) + LiquidFab( + icon = fabIcon, + onClick = { clicks++ }, + variant = LiquidButtonVariant.GlassProminent, + ) + } + } +} From d1740e18f791e3b014b5411565727ee625f6bb69 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 02:54:01 +0530 Subject: [PATCH 05/11] Add Liquid Glass surface/card container with native-parity rendering LiquidSurface + LiquidGlassContainer: commonMain expect/actual API mirroring toggle/slider. Android renders via the backdrop engine (drawBackdrop + lens/blur/vibrancy + highlight/shadow); iOS hosts a native UIVisualEffectView with iOS 26 UIGlassEffect (UIBlurEffect system-material fallback on older systems). Adds SurfaceShowcase sample. --- .../surface/LiquidSurface.android.kt | 131 +++++++++++++++ .../liquidkit/surface/LiquidSurface.kt | 94 +++++++++++ .../liquidkit/surface/LiquidSurface.ios.kt | 98 ++++++++++++ .../sample/showcase/SurfaceShowcase.kt | 150 ++++++++++++++++++ 4 files changed, 473 insertions(+) create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.android.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.ios.kt create mode 100644 sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/SurfaceShowcase.kt diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.android.kt new file mode 100644 index 0000000..c50fa6f --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.android.kt @@ -0,0 +1,131 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.surface + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.utils.InteractiveHighlight +import kotlin.math.abs +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.tanh + +@Composable +internal actual fun PlatformLiquidSurface( + modifier: Modifier, + shape: Shape, + cornerRadius: Dp, + tint: Color, + interactive: Boolean, + style: LiquidGlassStyle, + content: @Composable BoxScope.() -> Unit, +) { + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + val animationScope = rememberCoroutineScope() + val interactiveHighlight = remember(animationScope) { + InteractiveHighlight(animationScope = animationScope) + } + + Box( + modifier = modifier + .drawBackdrop( + backdrop = backdrop, + shape = { shape }, + effects = { + vibrancy() + blur(4f.dp.toPx()) + lens(cornerRadius.toPx().coerceAtMost(20f.dp.toPx()), 24f.dp.toPx()) + }, + highlight = { Highlight.Default }, + shadow = { Shadow(alpha = 0.18f) }, + innerShadow = { InnerShadow(radius = 6f.dp, alpha = 0.08f) }, + layerBlock = if (interactive) { + { + val width = size.width + val height = size.height + val progress = interactiveHighlight.pressProgress + val scale = lerp(1f, 1f + 4f.dp.toPx() / size.height, progress) + val maxOffset = size.minDimension + val initialDerivative = 0.05f + val offset = interactiveHighlight.offset + translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset) + translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset) + val maxDragScale = 4f.dp.toPx() / size.height + val offsetAngle = atan2(offset.y, offset.x) + scaleX = scale + maxDragScale * + abs(cos(offsetAngle) * offset.x / size.maxDimension) + scaleY = scale + maxDragScale * + abs(sin(offsetAngle) * offset.y / size.maxDimension) + } + } else { + null + }, + onDrawSurface = { + if (tint.isSpecified) { + drawRect(tint, blendMode = BlendMode.Hue) + drawRect(tint.copy(alpha = 0.55f)) + } else { + drawRect(style.containerColor) + } + }, + ) + .then( + if (interactive) { + Modifier + .then(interactiveHighlight.modifier) + .then(interactiveHighlight.gestureModifier) + } else { + Modifier + }, + ), + content = content, + ) +} + +@Composable +internal actual fun PlatformLiquidGlassContainer( + modifier: Modifier, + content: @Composable BoxScope.() -> Unit, +) { + // Capture whatever is drawn behind the container into a single shared layer, then expose it to + // nested glass children via LocalLiquidLayerBackdrop so they all sample the same region. + val sharedLayer: LayerBackdrop = rememberLayerBackdrop() + + Box( + modifier = modifier.layerBackdrop(sharedLayer), + ) { + CompositionLocalProvider(LocalLiquidLayerBackdrop provides sharedLayer) { + content() + } + } +} diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.kt new file mode 100644 index 0000000..a0b6832 --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.kt @@ -0,0 +1,94 @@ +package io.github.androidpoet.liquidkit.surface + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.unit.Dp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle + +/** + * A Liquid Glass card / container that renders genuine native glass behind arbitrary [content]. + * + * This is the cross-platform counterpart of iOS 26's `GlassEffectContainer` / + * `.glassEffect()` applied to a custom view: + * + * - On **Android** the [io.github.androidpoet.liquidkit] backdrop engine samples the content drawn + * behind the surface and applies refraction, blur, highlight and shadow (Kyant-style Liquid Glass). + * - On **iOS** a native `UIVisualEffectView` configured with the iOS 26 `UIGlassEffect` + * (falling back to a system blur material when `UIGlassEffect` is unavailable) is hosted via + * Compose `UIKitView` interop. The Android shader is never used on iOS. + * + * @param modifier Sizing / placement modifier applied to the surface. + * @param shape The clipping / refraction shape of the glass. Defaults to a rounded card. + * @param tint Optional color wash blended into the glass. [Color.Unspecified] applies no tint. + * @param interactive When `true` the surface gains the engine's interactive press / drag highlight + * on Android (a no-op on iOS, where the native material owns interaction). + * @param style The [LiquidGlassStyle] supplying corner radius and default tint when [shape] / [tint] + * are not overridden. + * @param content Content drawn on top of the glass. + */ +@Composable +public fun LiquidSurface( + modifier: Modifier = Modifier, + shape: Shape? = null, + tint: Color = Color.Unspecified, + interactive: Boolean = false, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, + content: @Composable BoxScope.() -> Unit, +) { + val resolvedShape = shape ?: RoundedCornerShape(style.cornerRadius) + PlatformLiquidSurface( + modifier = modifier, + shape = resolvedShape, + cornerRadius = style.cornerRadius, + tint = tint, + interactive = interactive, + style = style, + content = content, + ) +} + +@Composable +internal expect fun PlatformLiquidSurface( + modifier: Modifier, + shape: Shape, + cornerRadius: Dp, + tint: Color, + interactive: Boolean, + style: LiquidGlassStyle, + content: @Composable BoxScope.() -> Unit, +) + +/** + * Groups multiple [LiquidSurface] (or other glass) children so that, on Android, they sample a + * single shared backdrop region — the morphing / coherence idea behind iOS 26's + * `GlassEffectContainer`. + * + * The container itself is a transparent [Box]; it captures the content drawn behind it once and + * exposes that shared sampling layer to nested [LiquidSurface] children. On iOS each child hosts + * its own native material, so this is a plain grouping layout there. + * + * @param modifier Layout modifier for the group. + * @param content The glass children, typically one or more [LiquidSurface]. + */ +@Composable +public fun LiquidGlassContainer( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + PlatformLiquidGlassContainer( + modifier = modifier, + content = content, + ) +} + +@Composable +internal expect fun PlatformLiquidGlassContainer( + modifier: Modifier, + content: @Composable BoxScope.() -> Unit, +) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.ios.kt new file mode 100644 index 0000000..682160a --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.ios.kt @@ -0,0 +1,98 @@ +@file:OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) + +package io.github.androidpoet.liquidkit.surface + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.toUIColor +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents +import platform.Foundation.NSProcessInfo +import platform.UIKit.UIBlurEffect +import platform.UIKit.UIBlurEffectStyle +import platform.UIKit.UIGlassEffect +import platform.UIKit.UIGlassEffectStyle +import platform.UIKit.UIVisualEffect +import platform.UIKit.UIVisualEffectView + +/** + * Renders a genuine native iOS glass surface using a [UIVisualEffectView]. + * + * Prefers the iOS 26 `UIGlassEffect` (with tint + interactivity); on earlier systems it falls back + * to a `UIBlurEffect` system material. The Android refraction shader is never used here. + */ +@Composable +internal actual fun PlatformLiquidSurface( + modifier: Modifier, + shape: Shape, + cornerRadius: Dp, + tint: Color, + interactive: Boolean, + style: LiquidGlassStyle, + content: @Composable BoxScope.() -> Unit, +) { + val cornerPx = cornerRadius.value.toDouble() + val effect = remember(tint, interactive) { makeGlassVisualEffect(tint, interactive) } + + Box(modifier = modifier) { + UIKitView( + factory = { + UIVisualEffectView(effect = effect).apply { + layer.cornerRadius = cornerPx + layer.masksToBounds = true + setUserInteractionEnabled(false) + } + }, + modifier = Modifier.fillMaxSize(), + update = { view -> + view.effect = effect + view.layer.cornerRadius = cornerPx + }, + onRelease = {}, + properties = UIKitInteropProperties( + // Native glass sits behind Compose content; it must never intercept touches. + interactionMode = null, + isNativeAccessibilityEnabled = false, + ), + ) + // Compose content drawn on top of the native glass material. + content() + } +} + +@Composable +internal actual fun PlatformLiquidGlassContainer( + modifier: Modifier, + content: @Composable BoxScope.() -> Unit, +) { + // On iOS each LiquidSurface hosts its own native material, so the container is a plain group. + Box(modifier = modifier, content = content) +} + +private fun makeGlassVisualEffect(tint: Color, interactive: Boolean): UIVisualEffect { + if (isiOS26OrLater()) { + val glass = UIGlassEffect.effectWithStyle(UIGlassEffectStyle.UIGlassEffectStyleRegular) + glass.interactive = interactive + if (tint.isSpecified) { + glass.tintColor = tint.toUIColor() + } + return glass + } + // Fallback for pre-iOS 26 systems where UIGlassEffect is unavailable. + return UIBlurEffect.effectWithStyle(UIBlurEffectStyle.UIBlurEffectStyleSystemMaterial) +} + +private fun isiOS26OrLater(): Boolean = + NSProcessInfo.processInfo.operatingSystemVersion.useContents { majorVersion >= 26L } diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/SurfaceShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/SurfaceShowcase.kt new file mode 100644 index 0000000..5b8f84f --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/SurfaceShowcase.kt @@ -0,0 +1,150 @@ +package io.github.androidpoet.liquidkit.sample.showcase + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +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.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.surface.LiquidGlassContainer +import io.github.androidpoet.liquidkit.surface.LiquidSurface + +/** + * Self-contained demo of [LiquidSurface] / [LiquidGlassContainer] over a colorful backdrop, so the + * native glass refraction (Android engine / iOS UIGlassEffect) is clearly visible. + */ +@Composable +public fun SurfaceShowcase(modifier: Modifier = Modifier) { + var interactive by remember { mutableStateOf(true) } + + Column( + modifier = modifier + .fillMaxSize() + .background(showcaseBackground()) + .safeDrawingPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 28.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + BasicText( + text = "Liquid Surface", + modifier = Modifier.fillMaxWidth(), + style = TextStyle( + color = Color.White, + fontSize = 30.sp, + fontWeight = FontWeight.Bold, + ), + ) + + // A simple glass card with content on top. + LiquidSurface( + modifier = Modifier + .fillMaxWidth() + .height(140.dp), + ) { + CardLabel( + title = "Default card", + subtitle = "Native glass behind arbitrary content", + ) + } + + // Tinted, interactive glass card. + LiquidSurface( + modifier = Modifier + .fillMaxWidth() + .height(140.dp), + shape = RoundedCornerShape(36.dp), + tint = Color(0xFF4FC3F7), + interactive = interactive, + ) { + CardLabel( + title = "Tinted + interactive", + subtitle = "Press / drag on Android for the highlight", + ) + } + + // A container grouping two glass children that share one sampling region (Android). + LiquidGlassContainer( + modifier = Modifier + .fillMaxWidth() + .height(160.dp), + ) { + LiquidSurface( + modifier = Modifier + .fillMaxWidth(0.5f) + .height(120.dp) + .align(Alignment.CenterStart), + tint = Color(0xFFFFB74D), + ) { + CardLabel(title = "Grouped A", subtitle = "Shared region") + } + LiquidSurface( + modifier = Modifier + .fillMaxWidth(0.5f) + .height(120.dp) + .align(Alignment.CenterEnd), + tint = Color(0xFFBA68C8), + ) { + CardLabel(title = "Grouped B", subtitle = "Shared region") + } + } + } +} + +@Composable +private fun CardLabel( + title: String, + subtitle: String, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(18.dp), + verticalArrangement = Arrangement.Center, + ) { + BasicText( + text = title, + style = TextStyle( + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + BasicText( + text = subtitle, + style = TextStyle( + color = Color.White.copy(alpha = 0.85f), + fontSize = 13.sp, + ), + ) + } +} + +private fun showcaseBackground(): Brush = Brush.linearGradient( + colors = listOf( + Color(0xFF1A237E), + Color(0xFF6A1B9A), + Color(0xFFAD1457), + Color(0xFFEF6C00), + ), +) From 0a966c1f5f56e4d43e23ebd7c2212d3e9ea20045 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 02:56:02 +0530 Subject: [PATCH 06/11] Add Liquid Glass Stepper and Media control components LiquidStepper: glass increment/decrement stepper (commonMain expect, Android backdrop-engine glass buttons, iOS native UIStepper). LiquidMediaControl: glass media transport with play/pause toggle plus scrubber (Android backdrop LiquidButton + LiquidSlider, iOS native UISlider scrubber + glass UIButton with SF Symbol play/pause). Adds self-contained StepperMediaShowcase sample. --- .../play/LiquidMediaControl.android.kt | 78 +++++++++ .../stepper/LiquidStepper.android.kt | 92 ++++++++++ .../liquidkit/play/LiquidMediaControl.kt | 61 +++++++ .../liquidkit/stepper/LiquidStepper.kt | 59 +++++++ .../liquidkit/play/LiquidMediaControl.ios.kt | 160 ++++++++++++++++++ .../liquidkit/stepper/LiquidStepper.ios.kt | 86 ++++++++++ .../sample/showcase/StepperMediaShowcase.kt | 113 +++++++++++++ 7 files changed, 649 insertions(+) create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.android.kt create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.ios.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.ios.kt create mode 100644 sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/StepperMediaShowcase.kt diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.android.kt new file mode 100644 index 0000000..03735ac --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.android.kt @@ -0,0 +1,78 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.play + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidButton +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidSlider + +@Composable +internal actual fun PlatformLiquidMediaControl( + isPlaying: Boolean, + onPlayPauseToggle: (Boolean) -> Unit, + progress: Float, + onProgressChange: (Float) -> Unit, + modifier: Modifier, + progressRange: ClosedFloatingPointRange, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + Row( + modifier = modifier.alpha(if (enabled) 1f else 0.42f), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LiquidButton( + onClick = { if (enabled) onPlayPauseToggle(!isPlaying) }, + backdrop = backdrop, + isInteractive = enabled, + surfaceColor = style.selectedContainerColor, + ) { + Box( + modifier = Modifier.size(width = 22.dp, height = 24.dp), + contentAlignment = Alignment.Center, + ) { + // ▶ / ❚❚ as text glyphs to avoid an icon dependency. + BasicText( + text = if (isPlaying) "❚❚" else "▶", + style = TextStyle( + color = style.selectedContentColor, + fontSize = if (isPlaying) 16.sp else 20.sp, + fontWeight = FontWeight.Medium, + ), + ) + } + } + + LiquidSlider( + value = { progress }, + onValueChange = { if (enabled) onProgressChange(it) }, + valueRange = progressRange, + visibilityThreshold = (progressRange.endInclusive - progressRange.start) / 1_000f, + backdrop = backdrop, + modifier = Modifier.weight(1f), + ) + } +} diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt new file mode 100644 index 0000000..9b7145a --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt @@ -0,0 +1,92 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.stepper + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidButton + +@Composable +internal actual fun PlatformLiquidStepper( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier, + valueRange: ClosedFloatingPointRange, + step: Float, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + val canDecrement = enabled && value > valueRange.start + val canIncrement = enabled && value < valueRange.endInclusive + + Row( + modifier = modifier.alpha(if (enabled) 1f else 0.42f), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LiquidButton( + onClick = { + if (canDecrement) { + onValueChange((value - step).coerceIn(valueRange)) + } + }, + backdrop = backdrop, + isInteractive = canDecrement, + surfaceColor = style.containerColor, + modifier = Modifier.alpha(if (canDecrement) 1f else 0.42f), + ) { + StepperGlyph(text = "−", color = style.selectedContentColor) + } + + LiquidButton( + onClick = { + if (canIncrement) { + onValueChange((value + step).coerceIn(valueRange)) + } + }, + backdrop = backdrop, + isInteractive = canIncrement, + surfaceColor = style.containerColor, + modifier = Modifier.alpha(if (canIncrement) 1f else 0.42f), + ) { + StepperGlyph(text = "+", color = style.selectedContentColor) + } + } +} + +@Composable +private fun StepperGlyph(text: String, color: androidx.compose.ui.graphics.Color) { + Box( + modifier = Modifier.size(width = 20.dp, height = 24.dp), + contentAlignment = Alignment.Center, + ) { + BasicText( + text = text, + style = TextStyle( + color = color, + fontSize = 22.sp, + fontWeight = FontWeight.Medium, + ), + ) + } +} diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.kt new file mode 100644 index 0000000..5cda387 --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.kt @@ -0,0 +1,61 @@ +package io.github.androidpoet.liquidkit.play + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle + +/** + * A Liquid Glass media transport: a play/pause toggle button paired with a scrubber/progress slider. + * + * On Android the transport is rendered with the LiquidKit glass backdrop engine; on iOS it uses a + * genuine native `UISlider` scrubber plus a glass `UIButton` (SF Symbol play/pause) via UIKit + * interop so it adopts authentic system Liquid Glass on iOS 26. + * + * @param isPlaying whether the transport is currently in the playing state. + * @param onPlayPauseToggle invoked with the new playing state when the toggle is tapped. + * @param progress the current scrubber progress, coerced into [progressRange]. + * @param onProgressChange invoked while the user scrubs. + * @param modifier the [Modifier] to apply to this transport. + * @param progressRange the inclusive range the [progress] is coerced into. + * @param enabled whether the transport responds to user input. + * @param style the [LiquidGlassStyle] used to tint the glass surfaces. + */ +@Composable +public fun LiquidMediaControl( + isPlaying: Boolean, + onPlayPauseToggle: (Boolean) -> Unit, + progress: Float, + onProgressChange: (Float) -> Unit, + modifier: Modifier = Modifier, + progressRange: ClosedFloatingPointRange = 0f..1f, + enabled: Boolean = true, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, +) { + require(progressRange.start < progressRange.endInclusive) { + "LiquidMediaControl requires a non-empty progressRange." + } + + PlatformLiquidMediaControl( + isPlaying = isPlaying, + onPlayPauseToggle = onPlayPauseToggle, + progress = progress.coerceIn(progressRange), + onProgressChange = onProgressChange, + modifier = modifier, + progressRange = progressRange, + enabled = enabled, + style = style, + ) +} + +@Composable +internal expect fun PlatformLiquidMediaControl( + isPlaying: Boolean, + onPlayPauseToggle: (Boolean) -> Unit, + progress: Float, + onProgressChange: (Float) -> Unit, + modifier: Modifier, + progressRange: ClosedFloatingPointRange, + enabled: Boolean, + style: LiquidGlassStyle, +) diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.kt new file mode 100644 index 0000000..9dd73da --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.kt @@ -0,0 +1,59 @@ +package io.github.androidpoet.liquidkit.stepper + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle + +/** + * A Liquid Glass increment/decrement stepper. + * + * On Android it renders with the LiquidKit glass backdrop engine; on iOS it uses a genuine native + * `UIStepper` via UIKit interop so it picks up authentic system Liquid Glass on iOS 26. + * + * @param value the current value. + * @param onValueChange invoked with the new value when the user increments or decrements. + * @param modifier the [Modifier] to apply to this stepper. + * @param valueRange the inclusive range the [value] is coerced into. + * @param step the amount added or subtracted on each increment/decrement. + * @param enabled whether the stepper responds to user input. + * @param style the [LiquidGlassStyle] used to tint the glass surface. + */ +@Composable +public fun LiquidStepper( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + valueRange: ClosedFloatingPointRange = 0f..10f, + step: Float = 1f, + enabled: Boolean = true, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, +) { + require(valueRange.start < valueRange.endInclusive) { + "LiquidStepper requires a non-empty valueRange." + } + require(step > 0f) { + "LiquidStepper requires a positive step." + } + + PlatformLiquidStepper( + value = value.coerceIn(valueRange), + onValueChange = onValueChange, + modifier = modifier, + valueRange = valueRange, + step = step, + enabled = enabled, + style = style, + ) +} + +@Composable +internal expect fun PlatformLiquidStepper( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier, + valueRange: ClosedFloatingPointRange, + step: Float, + enabled: Boolean, + style: LiquidGlassStyle, +) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.ios.kt new file mode 100644 index 0000000..bea7685 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.ios.kt @@ -0,0 +1,160 @@ +package io.github.androidpoet.liquidkit.play + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.toUIColor +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCAction +import platform.Foundation.NSSelectorFromString +import platform.UIKit.UIButton +import platform.UIKit.UIButtonConfiguration +import platform.UIKit.UIColor +import platform.UIKit.UIControlEventValueChanged +import platform.UIKit.UIControlStateNormal +import platform.UIKit.UIImage +import platform.UIKit.UISlider +import platform.darwin.NSObject + +@OptIn(BetaInteropApi::class) +private class LiquidScrubberTarget( + var onValueChange: (Float) -> Unit, +) : NSObject() { + @ObjCAction + fun valueChanged(sender: UISlider) { + onValueChange(sender.value) + } +} + +@OptIn(BetaInteropApi::class) +private class LiquidPlayPauseTarget( + var onToggle: () -> Unit, +) : NSObject() { + @ObjCAction + fun tapped() { + onToggle() + } +} + +@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidMediaControl( + isPlaying: Boolean, + onPlayPauseToggle: (Boolean) -> Unit, + progress: Float, + onProgressChange: (Float) -> Unit, + modifier: Modifier, + progressRange: ClosedFloatingPointRange, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val currentOnProgressChange = rememberUpdatedState(onProgressChange) + val currentOnToggle = rememberUpdatedState(onPlayPauseToggle) + val currentIsPlaying = rememberUpdatedState(isPlaying) + + val scrubberTarget = remember { + LiquidScrubberTarget { currentOnProgressChange.value(it) } + } + scrubberTarget.onValueChange = { currentOnProgressChange.value(it) } + + val playPauseTarget = remember { + LiquidPlayPauseTarget { currentOnToggle.value(!currentIsPlaying.value) } + } + playPauseTarget.onToggle = { currentOnToggle.value(!currentIsPlaying.value) } + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // Genuine native glass play/pause button (SF Symbol, iOS 26 glass configuration). + UIKitView( + factory = { + val config = UIButtonConfiguration.glassButtonConfiguration() + UIButton().apply { + configuration = config + applyPlayPauseImage(currentIsPlaying.value) + this.enabled = enabled + tintColor = style.selectedContentColor.toUIColor() + backgroundColor = UIColor.clearColor + addTarget( + target = playPauseTarget, + action = NSSelectorFromString("tapped"), + forControlEvents = platform.UIKit.UIControlEventTouchUpInside, + ) + } + }, + modifier = Modifier.requiredSize(width = 56.dp, height = 44.dp), + update = { button -> + button.applyPlayPauseImage(currentIsPlaying.value) + button.enabled = enabled + button.tintColor = style.selectedContentColor.toUIColor() + }, + onRelease = {}, + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), + ) + + // Genuine native scrubber. + UIKitView( + factory = { + UISlider().apply { + minimumValue = progressRange.start + maximumValue = progressRange.endInclusive + setValue(progress, animated = false) + this.enabled = enabled + minimumTrackTintColor = style.selectedContentColor.copy(alpha = 0.58f).toUIColor() + maximumTrackTintColor = style.contentColor.copy(alpha = 0.16f).toUIColor() + thumbTintColor = UIColor.whiteColor + backgroundColor = UIColor.clearColor + addTarget( + target = scrubberTarget, + action = NSSelectorFromString("valueChanged:"), + forControlEvents = UIControlEventValueChanged, + ) + } + }, + modifier = Modifier.weight(1f).height(44.dp), + update = { slider -> + slider.minimumValue = progressRange.start + slider.maximumValue = progressRange.endInclusive + if (slider.value != progress) { + slider.setValue(progress, animated = true) + } + slider.enabled = enabled + slider.minimumTrackTintColor = + style.selectedContentColor.copy(alpha = if (enabled) 0.58f else 0.26f).toUIColor() + slider.maximumTrackTintColor = + style.contentColor.copy(alpha = if (enabled) 0.16f else 0.10f).toUIColor() + slider.thumbTintColor = UIColor.whiteColor + }, + onRelease = {}, + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), + ) + } +} + +private fun UIButton.applyPlayPauseImage(isPlaying: Boolean) { + val symbol = if (isPlaying) "pause.fill" else "play.fill" + setImage(UIImage.systemImageNamed(symbol), forState = UIControlStateNormal) +} diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.ios.kt new file mode 100644 index 0000000..3e96d34 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.ios.kt @@ -0,0 +1,86 @@ +package io.github.androidpoet.liquidkit.stepper + +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.toUIColor +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCAction +import platform.Foundation.NSSelectorFromString +import platform.UIKit.UIColor +import platform.UIKit.UIControlEventValueChanged +import platform.UIKit.UIStepper +import platform.darwin.NSObject + +@OptIn(BetaInteropApi::class) +private class LiquidStepperTarget( + var onValueChange: (Float) -> Unit, +) : NSObject() { + @ObjCAction + fun valueChanged(sender: UIStepper) { + onValueChange(sender.value.toFloat()) + } +} + +@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidStepper( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier, + valueRange: ClosedFloatingPointRange, + step: Float, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val currentOnValueChange = rememberUpdatedState(onValueChange) + val target = remember { + LiquidStepperTarget { currentOnValueChange.value(it) } + } + target.onValueChange = { currentOnValueChange.value(it) } + + UIKitView( + factory = { + UIStepper().apply { + minimumValue = valueRange.start.toDouble() + maximumValue = valueRange.endInclusive.toDouble() + stepValue = step.toDouble() + this.value = value.toDouble() + this.enabled = enabled + tintColor = style.selectedContentColor.toUIColor() + backgroundColor = UIColor.clearColor + addTarget( + target = target, + action = NSSelectorFromString("valueChanged:"), + forControlEvents = UIControlEventValueChanged, + ) + } + }, + modifier = modifier.height(44.dp), + update = { stepper -> + stepper.minimumValue = valueRange.start.toDouble() + stepper.maximumValue = valueRange.endInclusive.toDouble() + stepper.stepValue = step.toDouble() + if (stepper.value != value.toDouble()) { + stepper.value = value.toDouble() + } + stepper.enabled = enabled + stepper.tintColor = style.selectedContentColor.toUIColor() + }, + onRelease = {}, + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), + ) +} diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/StepperMediaShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/StepperMediaShowcase.kt new file mode 100644 index 0000000..5cffbde --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/StepperMediaShowcase.kt @@ -0,0 +1,113 @@ +package io.github.androidpoet.liquidkit.sample.showcase + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.text.BasicText +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LiquidKitTheme +import io.github.androidpoet.liquidkit.play.LiquidMediaControl +import io.github.androidpoet.liquidkit.stepper.LiquidStepper + +/** + * Self-contained showcase for [LiquidStepper] and [LiquidMediaControl]. + * + * Renders both new parity components over a colorful gradient so the Liquid Glass surfaces are + * visible. Drop this composable anywhere to preview the components in isolation. + */ +@Composable +public fun StepperMediaShowcase() { + LiquidKitTheme(style = LiquidGlassStyle.SurfaceDark) { + Column( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + listOf( + Color(0xFF1E2A78), + Color(0xFF6A1B9A), + Color(0xFFFF6F00), + ), + ), + ) + .safeDrawingPadding() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + SectionLabel("Liquid Stepper") + + var quantity by remember { mutableFloatStateOf(2f) } + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + ValueLabel("Quantity: ${quantity.toInt()}") + LiquidStepper( + value = quantity, + onValueChange = { quantity = it }, + valueRange = 0f..10f, + step = 1f, + ) + } + + Spacer(Modifier.height(8.dp)) + + SectionLabel("Liquid Media Control") + + var isPlaying by remember { mutableStateOf(false) } + var progress by remember { mutableFloatStateOf(0.35f) } + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + ValueLabel( + (if (isPlaying) "Playing" else "Paused") + + " • ${(progress * 100).toInt()}%", + ) + LiquidMediaControl( + isPlaying = isPlaying, + onPlayPauseToggle = { isPlaying = it }, + progress = progress, + onProgressChange = { progress = it }, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +@Composable +private fun SectionLabel(text: String) { + BasicText( + text = text, + style = TextStyle( + color = Color.White, + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + ), + ) +} + +@Composable +private fun ValueLabel(text: String) { + BasicText( + text = text, + style = TextStyle( + color = Color.White.copy(alpha = 0.85f), + fontSize = 15.sp, + ), + ) +} From c1842bd047bc49979e4c86386375ee215ca5d074 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 02:57:52 +0530 Subject: [PATCH 07/11] Add Liquid Glass TextField and SearchField components - commonMain field/ package with expect/actual mirroring toggle/slider - Android: glass surface (RoundedCornerShape) / capsule pill over the relocated backdrop engine wrapping BasicTextField - iOS: genuine native UITextField / UISearchTextField via UIKitView interop for authentic system Liquid Glass on iOS 26 - Self-contained FieldShowcase sample composable --- .../field/LiquidSearchField.android.kt | 157 ++++++++++++++++++ .../field/LiquidTextField.android.kt | 117 +++++++++++++ .../liquidkit/field/LiquidSearchField.kt | 53 ++++++ .../liquidkit/field/LiquidTextField.kt | 61 +++++++ .../liquidkit/field/LiquidSearchField.ios.kt | 101 +++++++++++ .../liquidkit/field/LiquidTextField.ios.kt | 107 ++++++++++++ .../sample/showcase/FieldShowcase.kt | 111 +++++++++++++ 7 files changed, 707 insertions(+) create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.android.kt create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.android.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.ios.kt create mode 100644 sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/FieldShowcase.kt diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.android.kt new file mode 100644 index 0000000..45a199f --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.android.kt @@ -0,0 +1,157 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.field + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.kyant.shapes.Capsule +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight + +@Composable +internal actual fun PlatformLiquidSearchField( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier, + placeholder: String, + onSearch: (String) -> Unit, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + Row( + modifier = modifier + .alpha(if (enabled) 1f else 0.42f) + .fillMaxWidth() + .drawBackdrop( + backdrop = backdrop, + shape = { Capsule() }, + effects = { + vibrancy() + blur(8f.dp.toPx()) + lens(8f.dp.toPx(), 16f.dp.toPx()) + }, + highlight = { Highlight.Default }, + onDrawSurface = { drawRect(style.containerColor) }, + ) + .height(44.dp) + .padding(horizontal = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + // Magnifier glyph drawn with Canvas so we need no icon dependency. + Box( + modifier = Modifier + .size(16.dp) + .androidMagnifier(style.contentColor), + ) + + Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { + if (query.isEmpty() && placeholder.isNotEmpty()) { + BasicText( + text = placeholder, + style = TextStyle(color = style.contentColor.copy(alpha = 0.6f), fontSize = 15.sp), + ) + } + BasicTextField( + value = query, + onValueChange = { if (enabled) onQueryChange(it) }, + enabled = enabled, + singleLine = true, + textStyle = TextStyle(color = style.selectedContentColor, fontSize = 15.sp), + cursorBrush = SolidColor(style.selectedContentColor), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { onSearch(query) }), + modifier = Modifier.fillMaxWidth(), + ) + } + + if (query.isNotEmpty()) { + Box( + modifier = Modifier + .size(18.dp) + .clickable( + interactionSource = null, + indication = null, + enabled = enabled, + role = Role.Button, + onClick = { onQueryChange("") }, + ) + .androidClearGlyph(style.contentColor), + ) + } + } +} + +private fun Modifier.androidMagnifier(color: Color): Modifier = + this.drawBehind { + val dim = size.minDimension + val r = dim * 0.32f + val stroke = dim * 0.12f + val center = Offset(size.width * 0.42f, size.height * 0.42f) + drawCircle(color = color, radius = r, center = center, style = Stroke(width = stroke)) + val start = Offset(center.x + r * 0.7f, center.y + r * 0.7f) + drawLine( + color = color, + start = start, + end = Offset(size.width * 0.92f, size.height * 0.92f), + strokeWidth = stroke, + ) + } + +private fun Modifier.androidClearGlyph(color: Color): Modifier = + this.drawBehind { + val dim = size.minDimension + val r = dim * 0.5f + drawCircle(color = color.copy(alpha = 0.30f), radius = r) + val inset = dim * 0.32f + val stroke = dim * 0.10f + drawLine( + color = color, + start = Offset(inset, inset), + end = Offset(size.width - inset, size.height - inset), + strokeWidth = stroke, + ) + drawLine( + color = color, + start = Offset(size.width - inset, inset), + end = Offset(inset, size.height - inset), + strokeWidth = stroke, + ) + } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.android.kt new file mode 100644 index 0000000..2400cb5 --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.android.kt @@ -0,0 +1,117 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.field + +import androidx.compose.foundation.Image +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.icon.LiquidIcon +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight + +@Composable +internal actual fun PlatformLiquidTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier, + placeholder: String, + leadingIcon: LiquidIcon?, + trailingIcon: LiquidIcon?, + singleLine: Boolean, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val isLight = !isSystemInDarkTheme() + + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + val fieldShape = RoundedCornerShape(style.cornerRadius) + + Row( + modifier = modifier + .alpha(if (enabled) 1f else 0.42f) + .fillMaxWidth() + .drawBackdrop( + backdrop = backdrop, + shape = { fieldShape }, + effects = { + vibrancy() + blur(8f.dp.toPx()) + lens(8f.dp.toPx(), 16f.dp.toPx()) + }, + highlight = { Highlight.Default }, + onDrawSurface = { drawRect(style.containerColor) }, + ) + .heightIn(min = 52.dp) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + leadingIcon?.vectorFor(selected = false)?.let { vector -> + Image( + painter = rememberVectorPainter(vector), + contentDescription = leadingIcon.contentDescription, + modifier = Modifier.size(20.dp), + colorFilter = ColorFilter.tint(style.contentColor), + ) + } + + Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { + if (value.isEmpty() && placeholder.isNotEmpty()) { + androidx.compose.foundation.text.BasicText( + text = placeholder, + style = TextStyle(color = style.contentColor.copy(alpha = 0.6f), fontSize = 16.sp), + ) + } + BasicTextField( + value = value, + onValueChange = { if (enabled) onValueChange(it) }, + enabled = enabled, + singleLine = singleLine, + textStyle = TextStyle(color = style.selectedContentColor, fontSize = 16.sp), + cursorBrush = SolidColor(style.selectedContentColor), + keyboardOptions = KeyboardOptions(imeAction = if (singleLine) ImeAction.Done else ImeAction.Default), + modifier = Modifier.fillMaxWidth(), + ) + } + + trailingIcon?.vectorFor(selected = false)?.let { vector -> + Image( + painter = rememberVectorPainter(vector), + contentDescription = trailingIcon.contentDescription, + modifier = Modifier.size(20.dp), + colorFilter = ColorFilter.tint(style.contentColor), + ) + } + } +} diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.kt new file mode 100644 index 0000000..16259ef --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.kt @@ -0,0 +1,53 @@ +package io.github.androidpoet.liquidkit.field + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle + +/** + * A Liquid Glass search pill, matching the iOS search-field idiom. + * + * On Android this renders a glass capsule (consistent with the sample's search pill) wrapping a + * Compose `BasicTextField` with a leading magnifier and a trailing clear affordance. On iOS it + * hosts a genuine native `UISearchTextField` via Compose `UIKitView` interop so it gets authentic + * system Liquid Glass on iOS 26. + * + * @param query current search query + * @param onQueryChange invoked with the new query as the user types + * @param placeholder hint shown while [query] is empty + * @param onSearch invoked with the current query when the user submits (keyboard search action) + * @param enabled whether the field accepts input + * @param style the Liquid Glass style applied to the pill + */ +@Composable +public fun LiquidSearchField( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeholder: String = "Search", + onSearch: (String) -> Unit = {}, + enabled: Boolean = true, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, +) { + PlatformLiquidSearchField( + query = query, + onQueryChange = onQueryChange, + modifier = modifier, + placeholder = placeholder, + onSearch = onSearch, + enabled = enabled, + style = style, + ) +} + +@Composable +internal expect fun PlatformLiquidSearchField( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier, + placeholder: String, + onSearch: (String) -> Unit, + enabled: Boolean, + style: LiquidGlassStyle, +) diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.kt new file mode 100644 index 0000000..64a00e6 --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.kt @@ -0,0 +1,61 @@ +package io.github.androidpoet.liquidkit.field + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle +import io.github.androidpoet.liquidkit.icon.LiquidIcon + +/** + * A Liquid Glass text input. + * + * On Android the field renders over the relocated Liquid Glass backdrop engine wrapping a + * Compose `BasicTextField`. On iOS it hosts a genuine native `UITextField` via Compose + * `UIKitView` interop so it picks up authentic system Liquid Glass on iOS 26. + * + * @param value current text value + * @param onValueChange invoked with the new text as the user types + * @param placeholder hint shown while [value] is empty + * @param leadingIcon optional icon rendered before the text + * @param trailingIcon optional icon rendered after the text + * @param singleLine whether the field is constrained to a single line + * @param enabled whether the field accepts input + * @param style the Liquid Glass style applied to the container + */ +@Composable +public fun LiquidTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeholder: String = "", + leadingIcon: LiquidIcon? = null, + trailingIcon: LiquidIcon? = null, + singleLine: Boolean = true, + enabled: Boolean = true, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, +) { + PlatformLiquidTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + singleLine = singleLine, + enabled = enabled, + style = style, + ) +} + +@Composable +internal expect fun PlatformLiquidTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier, + placeholder: String, + leadingIcon: LiquidIcon?, + trailingIcon: LiquidIcon?, + singleLine: Boolean, + enabled: Boolean, + style: LiquidGlassStyle, +) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt new file mode 100644 index 0000000..b1a5f43 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt @@ -0,0 +1,101 @@ +package io.github.androidpoet.liquidkit.field + +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.toUIColor +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCAction +import platform.Foundation.NSSelectorFromString +import platform.UIKit.UIColor +import platform.UIKit.UIControlEventEditingChanged +import platform.UIKit.UIReturnKeyType +import platform.UIKit.UISearchTextField +import platform.UIKit.UITextField +import platform.UIKit.UITextFieldDelegateProtocol +import platform.darwin.NSObject + +@OptIn(BetaInteropApi::class) +private class LiquidSearchFieldTarget( + var onQueryChange: (String) -> Unit, + var onSearch: (String) -> Unit, +) : NSObject(), UITextFieldDelegateProtocol { + @ObjCAction + fun editingChanged(sender: UITextField) { + onQueryChange(sender.text ?: "") + } + + // Keyboard "Search" / return button submits the query. + override fun textFieldShouldReturn(textField: UITextField): Boolean { + onSearch(textField.text ?: "") + textField.resignFirstResponder() + return true + } +} + +@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidSearchField( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier, + placeholder: String, + onSearch: (String) -> Unit, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val currentOnQueryChange = rememberUpdatedState(onQueryChange) + val currentOnSearch = rememberUpdatedState(onSearch) + val target = remember { + LiquidSearchFieldTarget( + onQueryChange = { currentOnQueryChange.value(it) }, + onSearch = { currentOnSearch.value(it) }, + ) + } + target.onQueryChange = { currentOnQueryChange.value(it) } + target.onSearch = { currentOnSearch.value(it) } + + UIKitView( + factory = { + // UISearchTextField is the genuine iOS search-field control: it brings the system + // magnifier glyph, clear button and (on iOS 26) authentic Liquid Glass styling. + UISearchTextField().apply { + text = query + this.placeholder = placeholder + backgroundColor = UIColor.clearColor + setOpaque(false) + textColor = style.selectedContentColor.toUIColor() + this.enabled = enabled + returnKeyType = UIReturnKeyType.UIReturnKeySearch + delegate = target + addTarget( + target = target, + action = NSSelectorFromString("editingChanged:"), + forControlEvents = UIControlEventEditingChanged, + ) + } + }, + modifier = modifier.height(44.dp), + update = { field -> + if (field.text != query) field.text = query + field.placeholder = placeholder + field.enabled = enabled + field.textColor = style.selectedContentColor.toUIColor() + }, + onRelease = { it.resignFirstResponder() }, + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), + ) +} diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.ios.kt new file mode 100644 index 0000000..cded73d --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.ios.kt @@ -0,0 +1,107 @@ +package io.github.androidpoet.liquidkit.field + +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.icon.LiquidIcon +import io.github.androidpoet.liquidkit.internal.toUIColor +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCAction +import platform.Foundation.NSSelectorFromString +import platform.UIKit.UIColor +import platform.UIKit.UIControlEventEditingChanged +import platform.UIKit.UIImage +import platform.UIKit.UIImageView +import platform.UIKit.UITextField +import platform.UIKit.UITextBorderStyle +import platform.UIKit.UITextFieldViewMode +import platform.UIKit.UIView +import platform.darwin.NSObject + +@OptIn(BetaInteropApi::class) +private class LiquidTextFieldTarget( + var onValueChange: (String) -> Unit, +) : NSObject() { + @ObjCAction + fun editingChanged(sender: UITextField) { + onValueChange(sender.text ?: "") + } +} + +@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier, + placeholder: String, + leadingIcon: LiquidIcon?, + trailingIcon: LiquidIcon?, + singleLine: Boolean, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val currentOnValueChange = rememberUpdatedState(onValueChange) + val target = remember { LiquidTextFieldTarget { currentOnValueChange.value(it) } } + target.onValueChange = { currentOnValueChange.value(it) } + + UIKitView( + factory = { + UITextField().apply { + text = value + this.placeholder = placeholder + // Borderless: the native field renders on top of the LiquidKit glass surface; + // on iOS 26 a borderless UITextField inherits the system glass treatment. + borderStyle = UITextBorderStyle.UITextBorderStyleNone + backgroundColor = UIColor.clearColor + setOpaque(false) + textColor = style.selectedContentColor.toUIColor() + this.enabled = enabled + leadingIcon?.sfImageView()?.let { + leftView = it + leftViewMode = UITextFieldViewMode.UITextFieldViewModeAlways + } + trailingIcon?.sfImageView()?.let { + rightView = it + rightViewMode = UITextFieldViewMode.UITextFieldViewModeAlways + } + addTarget( + target = target, + action = NSSelectorFromString("editingChanged:"), + forControlEvents = UIControlEventEditingChanged, + ) + } + }, + modifier = modifier.height(52.dp), + update = { field -> + if (field.text != value) field.text = value + field.placeholder = placeholder + field.enabled = enabled + field.textColor = style.selectedContentColor.toUIColor() + }, + onRelease = { it.resignFirstResponder() }, + properties = UIKitInteropProperties( + // Cooperative + overlay lets the native field receive taps and become first responder, + // so the system keyboard shows and typing works without manual focus wiring. + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), + ) +} + +@OptIn(ExperimentalForeignApi::class) +private fun LiquidIcon.sfImageView(): UIView? { + val name = iosSystemNameFor(selected = false) ?: return null + val image = UIImage.systemImageNamed(name) ?: return null + return UIImageView(image = image) +} diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/FieldShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/FieldShowcase.kt new file mode 100644 index 0000000..f628572 --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/FieldShowcase.kt @@ -0,0 +1,111 @@ +package io.github.androidpoet.liquidkit.sample.showcase + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.field.LiquidSearchField +import io.github.androidpoet.liquidkit.field.LiquidTextField + +/** + * Self-contained showcase for [LiquidTextField] and [LiquidSearchField]. + * + * Drop this composable anywhere (it manages its own state and background) to preview the + * Liquid Glass field components on Android and iOS. + */ +@Composable +public fun FieldShowcase(modifier: Modifier = Modifier) { + var name by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var query by remember { mutableStateOf("") } + var lastSearch by remember { mutableStateOf("") } + + Column( + modifier = modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + listOf(Color(0xFF1B3A6B), Color(0xFF6B3A8C), Color(0xFFB0507A)), + ), + ) + .verticalScroll(rememberScrollState()) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + SectionTitle("Liquid Glass Fields") + + SectionLabel("Text field") + LiquidTextField( + value = name, + onValueChange = { name = it }, + placeholder = "Full name", + style = LiquidGlassStyle.SurfaceDark, + modifier = Modifier.fillMaxWidth(), + ) + + SectionLabel("Text field (disabled)") + LiquidTextField( + value = email, + onValueChange = { email = it }, + placeholder = "you@example.com", + enabled = false, + style = LiquidGlassStyle.SurfaceDark, + modifier = Modifier.fillMaxWidth(), + ) + + SectionLabel("Search field") + LiquidSearchField( + query = query, + onQueryChange = { query = it }, + onSearch = { lastSearch = it }, + placeholder = "Search components", + style = LiquidGlassStyle.ControlDark, + modifier = Modifier.fillMaxWidth(), + ) + + if (lastSearch.isNotEmpty()) { + BasicText( + text = "Searched: \"$lastSearch\"", + style = TextStyle(color = Color.White.copy(alpha = 0.85f), fontSize = 13.sp), + ) + } + } +} + +@Composable +private fun SectionTitle(text: String) { + BasicText( + text = text, + style = TextStyle(color = Color.White, fontSize = 22.sp, fontWeight = FontWeight.Bold), + ) +} + +@Composable +private fun SectionLabel(text: String) { + BasicText( + text = text, + style = TextStyle( + color = Color.White.copy(alpha = 0.7f), + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + ), + ) +} From 4ba69c8d75a610ddc3eac5a49326e827ab8c6936 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 02:57:47 +0530 Subject: [PATCH 08/11] Add LiquidToolbar (top app bar) and LiquidSheet (bottom sheet) with native iOS glass --- .../liquidkit/sheet/LiquidSheet.android.kt | 87 ++++++++++ .../toolbar/LiquidToolbar.android.kt | 74 +++++++++ .../liquidkit/sheet/LiquidSheet.kt | 52 ++++++ .../liquidkit/toolbar/LiquidToolbar.kt | 48 ++++++ .../liquidkit/sheet/LiquidSheet.ios.kt | 110 +++++++++++++ .../liquidkit/toolbar/LiquidToolbar.ios.kt | 107 +++++++++++++ .../sample/showcase/ChromeShowcase.kt | 151 ++++++++++++++++++ 7 files changed, 629 insertions(+) create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.android.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.ios.kt create mode 100644 sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ChromeShowcase.kt diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt new file mode 100644 index 0000000..38e76ed --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt @@ -0,0 +1,87 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.sheet + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy + +@Composable +internal actual fun PlatformLiquidSheet( + visible: Boolean, + onDismiss: () -> Unit, + modifier: Modifier, + style: LiquidGlassStyle, + content: @Composable () -> Unit, +) { + if (!visible) return + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnClickOutside = true, + ), + ) { + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = + if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + // Bottom-aligned scrim that fills the dialog window. A tap on the scrim + // dismisses; the sheet surface itself sits at the bottom edge. + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.32f)), + contentAlignment = Alignment.BottomCenter, + ) { + val sheetShape = RoundedCornerShape( + topStart = style.cornerRadius, + topEnd = style.cornerRadius, + bottomStart = 0.dp, + bottomEnd = 0.dp, + ) + Box( + modifier = modifier + .fillMaxWidth() + .widthIn(max = 640.dp) + .drawBackdrop( + backdrop = backdrop, + shape = { sheetShape }, + effects = { + vibrancy() + blur(12f.dp.toPx()) + lens(24f.dp.toPx(), 24f.dp.toPx()) + }, + onDrawSurface = { drawRect(style.containerColor) }, + ) + .navigationBarsPadding() + .padding(20.dp), + ) { + content() + } + } + } +} diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.android.kt new file mode 100644 index 0000000..3081b6a --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.android.kt @@ -0,0 +1,74 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.toolbar + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy + +@Composable +internal actual fun PlatformLiquidToolbar( + title: @Composable () -> Unit, + modifier: Modifier, + leading: (@Composable () -> Unit)?, + trailing: (@Composable () -> Unit)?, + style: LiquidGlassStyle, +) { + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + Box( + modifier = modifier + .fillMaxWidth() + .height(56.dp) + .drawBackdrop( + backdrop = backdrop, + shape = { RoundedCornerShape(style.cornerRadius) }, + effects = { + vibrancy() + blur(8f.dp.toPx()) + lens(20f.dp.toPx(), 20f.dp.toPx()) + }, + onDrawSurface = { drawRect(style.containerColor) }, + ) + .padding(horizontal = 12.dp), + contentAlignment = Alignment.Center, + ) { + // Leading + trailing action slots pinned to the edges. + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Box(modifier = Modifier.widthIn(min = 40.dp)) { + leading?.invoke() + } + Box(modifier = Modifier.widthIn(min = 40.dp), contentAlignment = Alignment.CenterEnd) { + trailing?.invoke() + } + } + // Centered title. + Box(contentAlignment = Alignment.Center) { + title() + } + } +} diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.kt new file mode 100644 index 0000000..9134b27 --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.kt @@ -0,0 +1,52 @@ +package io.github.androidpoet.liquidkit.sheet + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle + +/** + * A Liquid Glass bottom sheet. + * + * When [visible] is `true` a glass surface slides up from the bottom of the screen + * hosting [content]. Tapping the scrim or requesting a dismiss invokes [onDismiss]. + * + * On Android the sheet is a glass-backed Compose surface (a dialog/popup primitive + * backed by the LiquidKit backdrop engine). On iOS it is a glass-backed surface using + * a native `UIVisualEffectView` (iOS 26 `UIGlassEffect`, system blur fallback). + * + * NOTE: full native sheet *presentation* (`UISheetPresentationController` with detents, + * drag-to-dismiss, etc.) is owned by the SwiftUI host. This in-Compose component renders + * a native glass-backed surface inside the Compose tree, which is the correct scope for a + * reusable cross-platform component. + * + * @param visible whether the sheet is shown. + * @param onDismiss invoked when the user dismisses the sheet (scrim tap / back). + * @param modifier applied to the sheet container. + * @param style glass styling. Defaults to [LiquidGlassStyle.Surface]. + * @param content the sheet body. + */ +@Composable +public fun LiquidSheet( + visible: Boolean, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + style: LiquidGlassStyle = LiquidGlassStyle.Surface, + content: @Composable () -> Unit, +) { + PlatformLiquidSheet( + visible = visible, + onDismiss = onDismiss, + modifier = modifier, + style = style, + content = content, + ) +} + +@Composable +internal expect fun PlatformLiquidSheet( + visible: Boolean, + onDismiss: () -> Unit, + modifier: Modifier, + style: LiquidGlassStyle, + content: @Composable () -> Unit, +) diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.kt new file mode 100644 index 0000000..6da87fc --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.kt @@ -0,0 +1,48 @@ +package io.github.androidpoet.liquidkit.toolbar + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle + +/** + * A Liquid Glass top app bar (toolbar / navigation bar). + * + * This is the Compose equivalent of iOS 26's auto-glass navigation bar: a glass + * surface that hosts a [title] alongside optional [leading] and [trailing] action + * slots. + * + * On Android the bar is rendered as a real glass surface backed by the LiquidKit + * backdrop engine. On iOS it is rendered with a genuine native [platform.UIKit.UINavigationBar] + * which automatically adopts Liquid Glass on iOS 26. + * + * @param title slot for the bar's title content (rendered centered/leading per platform). + * @param modifier applied to the bar container. + * @param leading optional slot for a leading action (e.g. a back button). + * @param trailing optional slot for a trailing action (e.g. an overflow / done button). + * @param style glass styling. Defaults to [LiquidGlassStyle.NavigationBar]. + */ +@Composable +public fun LiquidToolbar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + leading: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, + style: LiquidGlassStyle = LiquidGlassStyle.NavigationBar, +) { + PlatformLiquidToolbar( + title = title, + modifier = modifier, + leading = leading, + trailing = trailing, + style = style, + ) +} + +@Composable +internal expect fun PlatformLiquidToolbar( + title: @Composable () -> Unit, + modifier: Modifier, + leading: (@Composable () -> Unit)?, + trailing: (@Composable () -> Unit)?, + style: LiquidGlassStyle, +) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt new file mode 100644 index 0000000..9df7bdb --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt @@ -0,0 +1,110 @@ +package io.github.androidpoet.liquidkit.sheet + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import platform.UIKit.UIBlurEffect +import platform.UIKit.UIBlurEffectStyle +import platform.UIKit.UIVisualEffect +import platform.UIKit.UIVisualEffectView + +/** + * iOS [LiquidSheet] rendered as a native glass-backed surface using + * [UIVisualEffectView] with a system [UIBlurEffect] (`systemMaterial`). On iOS 26 + * that material already renders as Liquid Glass; the dedicated `UIGlassEffect` + * (not yet in the Kotlin/Native UIKit bindings) is best applied by the SwiftUI host. + * + * NOTE: native sheet *presentation* (`UISheetPresentationController` detents, + * grabber, interactive dismiss) is owned by the SwiftUI host. This component is + * the in-Compose glass surface, which is the right scope for a reusable component. + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidSheet( + visible: Boolean, + onDismiss: () -> Unit, + modifier: Modifier, + style: LiquidGlassStyle, + content: @Composable () -> Unit, +) { + if (!visible) return + + val currentOnDismiss = rememberUpdatedState(onDismiss) + val scrimInteraction = remember { MutableInteractionSource() } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.32f)) + .clickable( + interactionSource = scrimInteraction, + indication = null, + ) { currentOnDismiss.value() }, + contentAlignment = Alignment.BottomCenter, + ) { + Box( + modifier = modifier + .fillMaxWidth() + .widthIn(max = 640.dp), + contentAlignment = Alignment.TopCenter, + ) { + // Native glass background behind the Compose content. + UIKitView( + factory = { + UIVisualEffectView(effect = liquidGlassEffect()).apply { + clipsToBounds = true + layer.cornerRadius = style.cornerRadius.value.toDouble() + } + }, + modifier = Modifier.fillMaxSize(), + update = { view -> + view.layer.cornerRadius = style.cornerRadius.value.toDouble() + }, + onRelease = {}, + properties = UIKitInteropProperties( + interactionMode = null, + isNativeAccessibilityEnabled = false, + placedAsOverlay = false, + ), + ) + + // Compose content on top of the glass. + Box( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(20.dp), + ) { + content() + } + } + } +} + +/** + * Returns the system glass material for the sheet surface. + * + * iOS 26's `UIGlassEffect` is not yet exposed in the Kotlin/Native UIKit bindings, + * so this uses a system [UIBlurEffect] (`systemMaterial`) which on iOS 26 already + * renders as the Liquid Glass material; the true `UIGlassEffect` is best applied by + * the SwiftUI host that owns native presentation. See the iOS API gap note. + */ +private fun liquidGlassEffect(): UIVisualEffect = + UIBlurEffect.effectWithStyle(UIBlurEffectStyle.UIBlurEffectStyleSystemMaterial) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.ios.kt new file mode 100644 index 0000000..fb4fb80 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.ios.kt @@ -0,0 +1,107 @@ +package io.github.androidpoet.liquidkit.toolbar + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import kotlinx.cinterop.ExperimentalForeignApi +import platform.CoreGraphics.CGRectMake +import platform.UIKit.UIColor +import platform.UIKit.UINavigationBar +import platform.UIKit.UINavigationBarAppearance +import platform.UIKit.UINavigationItem + +/** + * iOS [LiquidToolbar] backed by a genuine native [UINavigationBar]. + * + * The native bar provides the glass surface (auto Liquid Glass on iOS 26, system + * translucent bar below) while the Compose [title]/[leading]/[trailing] slots are + * overlaid on top so callers keep an arbitrary Compose slot API. + */ +@OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidToolbar( + title: @Composable () -> Unit, + modifier: Modifier, + leading: (@Composable () -> Unit)?, + trailing: (@Composable () -> Unit)?, + style: LiquidGlassStyle, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(56.dp), + ) { + // Native glass background. + UIKitView( + factory = { + UINavigationBar(frame = CGRectMake(0.0, 0.0, 400.0, 56.0)).apply { + configureLiquidNavigationBar() + } + }, + modifier = Modifier.fillMaxSize(), + update = { bar -> bar.configureLiquidNavigationBar() }, + onRelease = {}, + properties = UIKitInteropProperties( + interactionMode = null, + isNativeAccessibilityEnabled = true, + placedAsOverlay = false, + ), + ) + + // Compose slot overlay. + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 12.dp), + contentAlignment = Alignment.Center, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Box(modifier = Modifier.widthIn(min = 40.dp)) { + leading?.invoke() + } + Box( + modifier = Modifier.widthIn(min = 40.dp), + contentAlignment = Alignment.CenterEnd, + ) { + trailing?.invoke() + } + } + Box(contentAlignment = Alignment.Center) { + title() + } + } + } +} + +private fun UINavigationBar.configureLiquidNavigationBar() { + // An empty nav item lets the bar render its (glass) chrome without native text, + // since the title is supplied by the Compose overlay. + setItems(listOf(UINavigationItem(title = "")), animated = false) + val appearance = UINavigationBarAppearance().apply { + // Default background opts the bar into the system material — Liquid Glass on iOS 26. + configureWithDefaultBackground() + } + standardAppearance = appearance + scrollEdgeAppearance = appearance + compactAppearance = appearance + translucent = true + backgroundColor = UIColor.clearColor +} diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ChromeShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ChromeShowcase.kt new file mode 100644 index 0000000..06527dd --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ChromeShowcase.kt @@ -0,0 +1,151 @@ +package io.github.androidpoet.liquidkit.sample.showcase + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +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.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.sheet.LiquidSheet +import io.github.androidpoet.liquidkit.toolbar.LiquidToolbar + +/** + * Self-contained showcase for [LiquidToolbar] and [LiquidSheet]. + * + * Drops into any host without touching the shared sample nav: renders a glass + * toolbar at the top with leading/trailing action slots, and a button that opens + * a glass bottom sheet. + */ +@Composable +public fun ChromeShowcase(modifier: Modifier = Modifier) { + var sheetVisible by remember { mutableStateOf(false) } + + Box( + modifier = modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + listOf(Color(0xFF6D83F2), Color(0xFFA66BF0), Color(0xFFF26D9C)), + ), + ), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + LiquidToolbar( + title = { + BasicText( + text = "Liquid Chrome", + style = TextStyle( + color = Color(0xFF101418), + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + }, + leading = { + BasicText( + text = "Back", + modifier = Modifier.clickable { /* no-op */ }, + style = TextStyle(color = Color(0xFF0A84FF), fontSize = 15.sp), + ) + }, + trailing = { + BasicText( + text = "Done", + modifier = Modifier.clickable { /* no-op */ }, + style = TextStyle( + color = Color(0xFF0A84FF), + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + }, + ) + + Spacer(Modifier.height(8.dp)) + + // "Open sheet" pill. + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color.White.copy(alpha = 0.38f), RoundedCornerShape(22.dp)) + .clickable { sheetVisible = true } + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center, + ) { + BasicText( + text = "Open glass sheet", + style = TextStyle( + color = Color(0xFF101418), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + } + } + + LiquidSheet( + visible = sheetVisible, + onDismiss = { sheetVisible = false }, + style = LiquidGlassStyle.Surface, + ) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + BasicText( + text = "Liquid Glass Sheet", + style = TextStyle( + color = Color(0xFF101418), + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ), + ) + BasicText( + text = "A glass-backed bottom sheet. Tap the scrim to dismiss.", + style = TextStyle(color = Color(0xFF4E5968), fontSize = 14.sp), + ) + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF0A84FF), RoundedCornerShape(20.dp)) + .clickable { sheetVisible = false } + .padding(vertical = 12.dp), + contentAlignment = Alignment.Center, + ) { + BasicText( + text = "Dismiss", + style = TextStyle( + color = Color.White, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + } + } + } + } +} From 0577e1dc0601e817451f36eeaa53c0b1482ebb54 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 03:01:43 +0530 Subject: [PATCH 09/11] Add Liquid Glass Menu/Dropdown + Picker components LiquidDropdownMenu/LiquidMenu and LiquidPicker with expect/actual: - commonMain: public API mirroring toggle/segmented structure - androidMain: glass surfaces via relocated backdrop engine - iosMain: native UIButton+UIMenu dropdown, native UIPickerView Self-contained MenuShowcase sample. --- .../dropdown/LiquidDropdownMenu.android.kt | 232 ++++++++++++++++++ .../liquidkit/picker/LiquidPicker.android.kt | 148 +++++++++++ .../liquidkit/dropdown/LiquidDropdownMenu.kt | 91 +++++++ .../liquidkit/picker/LiquidPicker.kt | 62 +++++ .../dropdown/LiquidDropdownMenu.ios.kt | 102 ++++++++ .../liquidkit/picker/LiquidPicker.ios.kt | 110 +++++++++ .../liquidkit/sample/showcase/MenuShowcase.kt | 152 ++++++++++++ 7 files changed, 897 insertions(+) create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.android.kt create mode 100644 liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.android.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.kt create mode 100644 liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt create mode 100644 liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.ios.kt create mode 100644 sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/MenuShowcase.kt diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.android.kt new file mode 100644 index 0000000..782a863 --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.android.kt @@ -0,0 +1,232 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.dropdown + +import androidx.compose.animation.core.spring +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +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.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow +import com.kyant.shapes.Capsule + +@Composable +internal actual fun PlatformLiquidDropdownMenu( + items: List>, + onSelect: (T) -> Unit, + modifier: Modifier, + label: String, + selectedKey: T?, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val isLight = !isSystemInDarkTheme() + var expanded by remember { mutableStateOf(false) } + + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + val anchorSurface = if (isLight) Color.Black.copy(0.05f) else Color.White.copy(0.08f) + + Box(modifier.alpha(if (enabled) 1f else 0.42f)) { + // Anchor: glass capsule button + Row( + Modifier + .drawBackdrop( + backdrop = backdrop, + shape = { Capsule() }, + effects = { + vibrancy() + blur(2f.dp.toPx()) + lens(12f.dp.toPx(), 24f.dp.toPx()) + }, + highlight = { Highlight.Default }, + shadow = { Shadow(alpha = 0.12f) }, + onDrawSurface = { drawRect(anchorSurface) }, + ) + .clickable( + interactionSource = null, + indication = null, + enabled = enabled, + role = Role.DropdownList, + onClick = { expanded = true }, + ) + .height(44.dp) + .padding(horizontal = 18.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + BasicText( + text = label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + color = style.selectedContentColor, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + BasicText( + text = "▾", + style = TextStyle(color = style.contentColor, fontSize = 12.sp), + ) + } + + if (expanded) { + val transitionState = remember { MutableTransitionState(false) } + transitionState.targetState = true + + Popup( + alignment = Alignment.TopStart, + offset = IntOffset(0, with(androidx.compose.ui.platform.LocalDensity.current) { 50.dp.roundToPx() }), + onDismissRequest = { expanded = false }, + properties = PopupProperties(focusable = true), + ) { + AnimatedVisibility( + visibleState = transitionState, + enter = fadeIn(spring()) + scaleIn(spring(), initialScale = 0.9f, transformOrigin = TransformOrigin(0f, 0f)), + exit = fadeOut() + scaleOut(targetScale = 0.9f), + ) { + DropdownGlassSurface( + items = items, + selectedKey = selectedKey, + backdrop = backdrop, + style = style, + isLight = isLight, + onSelect = { + expanded = false + onSelect(it) + }, + ) + } + } + } + } +} + +@Composable +private fun DropdownGlassSurface( + items: List>, + selectedKey: T?, + backdrop: Backdrop, + style: LiquidGlassStyle, + isLight: Boolean, + onSelect: (T) -> Unit, +) { + val menuSurface = if (isLight) Color.White.copy(0.55f) else Color.Black.copy(0.45f) + androidx.compose.foundation.layout.Column( + Modifier + .widthIn(min = 200.dp) + .drawBackdrop( + backdrop = backdrop, + shape = { RoundedCornerShape(20.dp) }, + effects = { + vibrancy() + blur(12f.dp.toPx()) + lens(10f.dp.toPx(), 20f.dp.toPx()) + }, + highlight = { Highlight.Default }, + shadow = { Shadow(alpha = 0.22f) }, + innerShadow = { InnerShadow(radius = 6f.dp, alpha = 0.10f) }, + onDrawSurface = { drawRect(menuSurface) }, + ) + .padding(6.dp), + ) { + items.forEach { item -> + val isSelected = selectedKey != null && item.key == selectedKey + val contentColor = if (isSelected) style.selectedContentColor else style.contentColor + Row( + Modifier + .fillMaxWidth() + .clickable( + interactionSource = null, + indication = null, + enabled = item.enabled, + role = Role.Button, + onClick = { onSelect(item.key) }, + ) + .alpha(if (item.enabled) 1f else 0.4f) + .padding(horizontal = 12.dp, vertical = 11.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + item.icon?.vectorFor(isSelected)?.let { vector -> + Image( + painter = rememberVectorPainter(vector), + contentDescription = item.icon.contentDescription, + modifier = Modifier.size(18.dp), + colorFilter = ColorFilter.tint(contentColor), + ) + } + BasicText( + text = item.label, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + color = contentColor, + fontSize = 15.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, + ), + ) + Spacer(Modifier.width(8.dp)) + if (isSelected) { + BasicText( + text = "✓", + style = TextStyle(color = style.selectedContentColor, fontSize = 14.sp, fontWeight = FontWeight.Bold), + ) + } + } + } + } +} diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.android.kt new file mode 100644 index 0000000..061072b --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.android.kt @@ -0,0 +1,148 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.picker + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.kyant.shapes.Capsule +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCombinedBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vibrancy +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow + +private val RowHeight = 44.dp +private val WheelHeight = RowHeight * 5 + +@Composable +internal actual fun PlatformLiquidPicker( + options: List>, + selectedKey: T, + onSelected: (T) -> Unit, + modifier: Modifier, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val isLight = !isSystemInDarkTheme() + val containerColor = if (isLight) Color.White.copy(0.42f) else Color.Black.copy(0.40f) + + val layerCapture = LocalLiquidLayerBackdrop.current + val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } + val outerBackdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop + + val trackLayer = rememberLayerBackdrop() + val listState = rememberLazyListState() + val selectedIndex = options.indexOfFirst { it.key == selectedKey }.coerceAtLeast(0) + + LaunchedEffect(selectedIndex) { + // Center the selected row within the 5-row wheel (2 rows above). + listState.animateScrollToItem(selectedIndex.coerceAtLeast(0)) + } + + Box( + modifier + .alpha(if (enabled) 1f else 0.42f) + .fillMaxWidth() + .height(WheelHeight), + contentAlignment = Alignment.Center, + ) { + // Glass container that the center selection refracts over. + LazyColumn( + modifier = Modifier + .layerBackdrop(trackLayer) + .drawBackdrop( + backdrop = outerBackdrop, + shape = { com.kyant.shapes.RoundedRectangle(20.dp) }, + effects = { + vibrancy() + blur(8f.dp.toPx()) + lens(8f.dp.toPx(), 16f.dp.toPx()) + }, + onDrawSurface = { drawRect(containerColor) }, + ) + .fillMaxWidth() + .height(WheelHeight), + state = listState, + contentPadding = PaddingValues(vertical = RowHeight * 2), + ) { + items(options, key = { it.key }) { option -> + val isSelected = option.key == selectedKey + Box( + Modifier + .fillMaxWidth() + .height(RowHeight) + .clickable( + interactionSource = null, + indication = null, + enabled = enabled, + role = Role.Button, + onClick = { onSelected(option.key) }, + ), + contentAlignment = Alignment.Center, + ) { + BasicText( + text = option.label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + color = if (isSelected) style.selectedContentColor else style.contentColor, + fontSize = if (isSelected) 19.sp else 17.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + textAlign = TextAlign.Center, + ), + ) + } + } + } + + // Floating glass selection band over the center row. + Box( + Modifier + .drawBackdrop( + backdrop = rememberCombinedBackdrop(outerBackdrop, trackLayer), + shape = { Capsule() }, + effects = { + lens(6f.dp.toPx(), 12f.dp.toPx(), chromaticAberration = true) + }, + highlight = { Highlight.Default }, + shadow = { Shadow(alpha = 0.12f) }, + onDrawSurface = { + val surfaceColor = if (isLight) Color.Black.copy(0.05f) else Color.White.copy(0.08f) + drawRect(surfaceColor) + }, + ) + .fillMaxWidth() + .height(RowHeight), + ) + } +} diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.kt new file mode 100644 index 0000000..eed1815 --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.kt @@ -0,0 +1,91 @@ +package io.github.androidpoet.liquidkit.dropdown + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle +import io.github.androidpoet.liquidkit.icon.LiquidIcon + +/** + * A single selectable entry in a [LiquidMenu] / [LiquidDropdownMenu]. + * + * @param key Stable identity returned through `onSelect`. + * @param label Text shown for the item. + * @param icon Optional leading icon (Compose vector on Android, SF Symbol on iOS). + * @param enabled When false the item is shown dimmed and is not selectable. + */ +public data class LiquidMenuItem( + public val key: T, + public val label: String, + public val icon: LiquidIcon? = null, + public val enabled: Boolean = true, +) + +/** + * A glass dropdown menu presented from an anchor. + * + * On Android the popup container is rendered as a Liquid Glass surface. On iOS it + * uses a genuine native `UIButton` with `showsMenuAsPrimaryAction` + `UIMenu` so it + * picks up authentic system Liquid Glass on iOS 26. + * + * @param items The selectable entries. + * @param onSelect Invoked with the [LiquidMenuItem.key] of the chosen entry. + * @param modifier Modifier applied to the anchor. + * @param label Text shown on the anchor button. + * @param selectedKey Optional currently-selected key, used to show a checkmark / state. + * @param enabled Whether the whole control is interactive. + * @param style Glass style tokens. + */ +@Composable +public fun LiquidDropdownMenu( + items: List>, + onSelect: (T) -> Unit, + modifier: Modifier = Modifier, + label: String = "Menu", + selectedKey: T? = null, + enabled: Boolean = true, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, +) { + require(items.isNotEmpty()) { "LiquidDropdownMenu requires at least one item." } + + PlatformLiquidDropdownMenu( + items = items, + onSelect = onSelect, + modifier = modifier, + label = label, + selectedKey = selectedKey, + enabled = enabled, + style = style, + ) +} + +/** Alias for [LiquidDropdownMenu], matching the SwiftUI `Menu` naming. */ +@Composable +public fun LiquidMenu( + items: List>, + onSelect: (T) -> Unit, + modifier: Modifier = Modifier, + label: String = "Menu", + selectedKey: T? = null, + enabled: Boolean = true, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, +): Unit = LiquidDropdownMenu( + items = items, + onSelect = onSelect, + modifier = modifier, + label = label, + selectedKey = selectedKey, + enabled = enabled, + style = style, +) + +@Composable +internal expect fun PlatformLiquidDropdownMenu( + items: List>, + onSelect: (T) -> Unit, + modifier: Modifier, + label: String, + selectedKey: T?, + enabled: Boolean, + style: LiquidGlassStyle, +) diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.kt new file mode 100644 index 0000000..8b0b215 --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.kt @@ -0,0 +1,62 @@ +package io.github.androidpoet.liquidkit.picker + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle + +/** + * A single value option in a [LiquidPicker]. + * + * @param key Stable identity returned through `onSelected`. + * @param label Text shown for the option. + */ +public data class LiquidPickerOption( + public val key: T, + public val label: String, +) + +/** + * A glass value picker for selecting one option from a list. + * + * On Android it renders a Liquid Glass wheel/list selector. On iOS it uses a genuine + * native `UIPickerView` via Compose `UIKitView` interop so it gets authentic system + * Liquid Glass on iOS 26. + * + * @param options The selectable options. + * @param selectedKey The currently selected key. + * @param onSelected Invoked with the [LiquidPickerOption.key] of the chosen option. + * @param modifier Modifier applied to the picker. + * @param enabled Whether the picker is interactive. + * @param style Glass style tokens. + */ +@Composable +public fun LiquidPicker( + options: List>, + selectedKey: T, + onSelected: (T) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + style: LiquidGlassStyle = LocalLiquidGlassStyle.current, +) { + require(options.isNotEmpty()) { "LiquidPicker requires at least one option." } + + PlatformLiquidPicker( + options = options, + selectedKey = selectedKey, + onSelected = onSelected, + modifier = modifier, + enabled = enabled, + style = style, + ) +} + +@Composable +internal expect fun PlatformLiquidPicker( + options: List>, + selectedKey: T, + onSelected: (T) -> Unit, + modifier: Modifier, + enabled: Boolean, + style: LiquidGlassStyle, +) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt new file mode 100644 index 0000000..79b12ae --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt @@ -0,0 +1,102 @@ +package io.github.androidpoet.liquidkit.dropdown + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.toUIColor +import kotlinx.cinterop.ExperimentalForeignApi +import platform.UIKit.UIAction +import platform.UIKit.UIButton +import platform.UIKit.UIButtonConfiguration +import platform.UIKit.UIControlStateNormal +import platform.UIKit.UIImage +import platform.UIKit.UIMenu +import platform.UIKit.UIMenuElement +import platform.UIKit.UIMenuElementState +import platform.UIKit.UIMenuElementAttributesDisabled + +/** + * Genuine native iOS dropdown: a [UIButton] with `showsMenuAsPrimaryAction = true` + * presenting a [UIMenu]. On iOS 26 this picks up authentic system Liquid Glass. + */ +@OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidDropdownMenu( + items: List>, + onSelect: (T) -> Unit, + modifier: Modifier, + label: String, + selectedKey: T?, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val currentOnSelect = rememberUpdatedState(onSelect) + val currentItems = rememberUpdatedState(items) + val currentSelected = rememberUpdatedState(selectedKey) + + UIKitView( + factory = { + UIButton().apply { + showsMenuAsPrimaryAction = true + configuration = UIButtonConfiguration.tintedButtonConfiguration() + } + }, + modifier = modifier.height(44.dp).widthIn(min = 120.dp), + update = { button -> + button.setTitle(label, forState = UIControlStateNormal) + button.setTitleColor(style.selectedContentColor.toUIColor(), forState = UIControlStateNormal) + button.tintColor = style.selectedContentColor.toUIColor() + button.enabled = enabled + button.alpha = if (enabled) 1.0 else 0.42 + button.setOpaque(false) + button.menu = buildMenu( + items = currentItems.value, + selectedKey = currentSelected.value, + onSelect = { currentOnSelect.value(it) }, + ) + }, + onRelease = {}, + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), + ) +} + +@OptIn(ExperimentalForeignApi::class) +private fun buildMenu( + items: List>, + selectedKey: T?, + onSelect: (T) -> Unit, +): UIMenu { + val actions: List = items.map { item -> + val image: UIImage? = item.icon?.iosSystemNameFor(selectedKey != null && item.key == selectedKey) + ?.let { UIImage.systemImageNamed(it) } + UIAction.actionWithTitle( + title = item.label, + image = image, + identifier = null, + handler = { onSelect(item.key) }, + ).apply { + state = if (selectedKey != null && item.key == selectedKey) { + UIMenuElementState.UIMenuElementStateOn + } else { + UIMenuElementState.UIMenuElementStateOff + } + if (!item.enabled) { + attributes = UIMenuElementAttributesDisabled + } + } + } + return UIMenu.menuWithChildren(actions) +} diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.ios.kt new file mode 100644 index 0000000..897a41d --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.ios.kt @@ -0,0 +1,110 @@ +package io.github.androidpoet.liquidkit.picker + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.androidpoet.liquidkit.LiquidGlassStyle +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCSignatureOverride +import platform.UIKit.UIColor +import platform.UIKit.UIPickerView +import platform.UIKit.UIPickerViewDataSourceProtocol +import platform.UIKit.UIPickerViewDelegateProtocol +import platform.darwin.NSInteger +import platform.darwin.NSObject + +private class LiquidPickerCoordinator( + var titles: List, + var onRowSelected: (Int) -> Unit, +) : NSObject(), UIPickerViewDataSourceProtocol, UIPickerViewDelegateProtocol { + + override fun numberOfComponentsInPickerView(pickerView: UIPickerView): NSInteger = 1L + + override fun pickerView( + pickerView: UIPickerView, + numberOfRowsInComponent: NSInteger, + ): NSInteger = titles.size.toLong() + + @ObjCSignatureOverride + override fun pickerView( + pickerView: UIPickerView, + titleForRow: NSInteger, + forComponent: NSInteger, + ): String? = titles.getOrNull(titleForRow.toInt()) + + @ObjCSignatureOverride + override fun pickerView( + pickerView: UIPickerView, + didSelectRow: NSInteger, + inComponent: NSInteger, + ) { + onRowSelected(didSelectRow.toInt()) + } +} + +/** + * Genuine native iOS picker: a [UIPickerView] driven by a Kotlin coordinator acting as + * data source + delegate. On iOS 26 it adopts the authentic system Liquid Glass styling. + */ +@OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +internal actual fun PlatformLiquidPicker( + options: List>, + selectedKey: T, + onSelected: (T) -> Unit, + modifier: Modifier, + enabled: Boolean, + style: LiquidGlassStyle, +) { + val currentOnSelected = rememberUpdatedState(onSelected) + val currentOptions = rememberUpdatedState(options) + + val coordinator = remember { + LiquidPickerCoordinator( + titles = options.map { it.label }, + onRowSelected = { index -> + currentOptions.value.getOrNull(index)?.key?.let(currentOnSelected.value) + }, + ) + } + coordinator.titles = options.map { it.label } + coordinator.onRowSelected = { index -> + currentOptions.value.getOrNull(index)?.key?.let(currentOnSelected.value) + } + + val selectedIndex = options.indexOfFirst { it.key == selectedKey }.coerceAtLeast(0) + + UIKitView( + factory = { + UIPickerView().apply { + dataSource = coordinator + delegate = coordinator + backgroundColor = UIColor.clearColor + setOpaque(false) + } + }, + modifier = modifier.fillMaxWidth().height(216.dp), + update = { picker -> + picker.userInteractionEnabled = enabled + picker.alpha = if (enabled) 1.0 else 0.42 + picker.reloadAllComponents() + if (picker.selectedRowInComponent(0) != selectedIndex.toLong()) { + picker.selectRow(selectedIndex.toLong(), inComponent = 0, animated = false) + } + }, + onRelease = {}, + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), + ) +} diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/MenuShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/MenuShowcase.kt new file mode 100644 index 0000000..35f7bf5 --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/MenuShowcase.kt @@ -0,0 +1,152 @@ +package io.github.androidpoet.liquidkit.sample.showcase + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +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.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.dropdown.LiquidDropdownMenu +import io.github.androidpoet.liquidkit.dropdown.LiquidMenuItem +import io.github.androidpoet.liquidkit.picker.LiquidPicker +import io.github.androidpoet.liquidkit.picker.LiquidPickerOption + +private enum class SortOrder(val label: String) { + Newest("Newest"), + Oldest("Oldest"), + Name("Name (A-Z)"), + Size("Size"), +} + +private enum class TimeZone(val label: String) { + Pacific("Pacific Time"), + Mountain("Mountain Time"), + Central("Central Time"), + Eastern("Eastern Time"), + UTC("UTC"), +} + +/** + * Self-contained showcase for [LiquidDropdownMenu] and [LiquidPicker]. + * + * Android renders glass surfaces; iOS uses genuine native UIButton+UIMenu / UIPickerView. + */ +@Composable +public fun MenuShowcase(modifier: Modifier = Modifier) { + var sort by remember { mutableStateOf(SortOrder.Newest) } + var zone by remember { mutableStateOf(TimeZone.Pacific) } + + val sortItems = remember { + SortOrder.entries.map { LiquidMenuItem(key = it, label = it.label) } + } + val zoneOptions = remember { + TimeZone.entries.map { LiquidPickerOption(key = it, label = it.label) } + } + + Column( + modifier = modifier + .fillMaxSize() + .background(showcaseBackground()) + .safeDrawingPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(28.dp), + ) { + ShowcaseTitle( + title = "Menu & Picker", + subtitle = "LiquidDropdownMenu and LiquidPicker — glass on Android, native on iOS.", + ) + + ShowcaseSection(title = "Dropdown menu", value = sort.label) { + LiquidDropdownMenu( + items = sortItems, + onSelect = { sort = it }, + label = "Sort: ${sort.label}", + selectedKey = sort, + modifier = Modifier.fillMaxWidth(), + ) + } + + ShowcaseSection(title = "Value picker", value = zone.label) { + LiquidPicker( + options = zoneOptions, + selectedKey = zone, + onSelected = { zone = it }, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@Composable +private fun ShowcaseTitle(title: String, subtitle: String) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + BasicText( + text = title, + style = TextStyle( + color = Color(0xFF070707), + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + ), + ) + BasicText( + text = subtitle, + style = TextStyle(color = Color(0xFF4A4A4A), fontSize = 15.sp, lineHeight = 21.sp), + ) + } +} + +@Composable +private fun ShowcaseSection( + title: String, + value: String, + content: @Composable () -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + BasicText( + text = title, + style = TextStyle( + color = Color(0xFF070707), + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + content() + BasicText( + text = "Selected: $value", + style = TextStyle(color = Color(0xFF555555), fontSize = 13.sp), + ) + } +} + +private fun showcaseBackground(): Brush = Brush.linearGradient( + colors = listOf( + Color(0xFFF7F7F7), + Color(0xFFEDEDED), + Color(0xFFDADADA), + ), +) From fba1c21f658a7728a4565d4588e7c5d7282a481b Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 03:23:57 +0530 Subject: [PATCH 10/11] Finish LiquidKit: full component set, Maven Central, docs, tooling - Wire all six component showcases into the sample app via a navigable component gallery (buttons, surfaces, menus/pickers, fields, stepper/media, toolbar/sheet) - Switch publishing to the Sonatype Central Portal with sources/javadoc jars, full POM metadata and env-driven signing; add tag-driven release workflow - Align the iOS sheet to the typed UIGlassEffect binding (iOS 26) with a UIBlurEffect systemMaterial fallback, matching the surface implementation - Harden the iOS sample prebuild to use the Gradle wrapper - Add Spotless (ktlint), Detekt and Kover quality gates and wire them into CI - Add a Nextra docs site (intro, install, architecture, per-component pages) and a Pages deploy workflow - Refresh the README install snippet, component list and architecture note --- .editorconfig | 7 + .github/workflows/ci.yml | 25 + .github/workflows/docs.yml | 50 + .github/workflows/publish.yml | 20 +- README.md | 80 +- THIRD_PARTY_NOTICES.md | 2 +- build.gradle.kts | 109 + .../liquidkit/build/LiquidKitConfiguration.kt | 4 + config/detekt/detekt.yml | 45 + gradle.properties | 19 + gradle/libs.versions.toml | 11 + .../LiquidKitSample.xcodeproj/project.pbxproj | 2 +- liquidkit-navigation3/build.gradle.kts | 22 +- .../navigation3/LiquidNav3TabScaffold.kt | 112 +- .../navigation3/LiquidNav3StateTest.kt | 11 +- liquidkit/build.gradle.kts | 18 +- .../liquidkit/button/LiquidButton.android.kt | 13 +- .../liquidkit/button/LiquidFab.android.kt | 2 +- .../dropdown/LiquidDropdownMenu.android.kt | 44 +- .../field/LiquidSearchField.android.kt | 60 +- .../field/LiquidTextField.android.kt | 35 +- .../liquidkit/internal/AndroidLiquidGlass.kt | 76 +- .../LiquidBottomNavigation.android.kt | 45 +- .../liquidkit/picker/LiquidPicker.android.kt | 46 +- .../play/LiquidMediaControl.android.kt | 12 +- .../LiquidSegmentedControl.android.kt | 32 +- .../liquidkit/sheet/LiquidSheet.android.kt | 59 +- .../liquidkit/slider/LiquidSlider.android.kt | 2 +- .../stepper/LiquidStepper.android.kt | 11 +- .../surface/LiquidSurface.android.kt | 110 +- .../liquidkit/toggle/LiquidToggle.android.kt | 2 +- .../toolbar/LiquidToolbar.android.kt | 28 +- .../androidpoet/liquidkit/LiquidGlassStyle.kt | 91 +- .../liquidkit/LiquidGlassStyleBuilder.kt | 15 +- .../liquidkit/dropdown/LiquidDropdownMenu.kt | 19 +- .../liquidkit/navigation/LiquidTabScaffold.kt | 11 +- .../liquidkit/surface/LiquidSurface.kt | 2 +- .../liquidkit/LiquidGlassStyleTest.kt | 11 +- .../liquidkit/button/LiquidButton.ios.kt | 18 +- .../liquidkit/button/LiquidFab.ios.kt | 18 +- .../dropdown/LiquidDropdownMenu.ios.kt | 66 +- .../liquidkit/field/LiquidSearchField.ios.kt | 27 +- .../liquidkit/field/LiquidTextField.ios.kt | 17 +- .../liquidkit/internal/IosLiquidGlass.kt | 13 +- .../navigation/LiquidBottomNavigation.ios.kt | 60 +- .../liquidkit/picker/LiquidPicker.ios.kt | 41 +- .../liquidkit/play/LiquidMediaControl.ios.kt | 36 +- .../segmented/LiquidSegmentedControl.ios.kt | 23 +- .../liquidkit/sheet/LiquidSheet.ios.kt | 75 +- .../liquidkit/slider/LiquidSlider.ios.kt | 18 +- .../liquidkit/stepper/LiquidStepper.ios.kt | 18 +- .../liquidkit/surface/LiquidSurface.ios.kt | 11 +- .../liquidkit/toggle/LiquidToggle.ios.kt | 18 +- .../liquidkit/toolbar/LiquidToolbar.ios.kt | 35 +- .../liquidkit/sample/MainActivity.kt | 37 +- .../liquidkit/sample/LiquidKitSampleApp.kt | 513 +- .../liquidkit/sample/SampleIcons.kt | 275 +- .../sample/showcase/ButtonShowcase.kt | 82 +- .../sample/showcase/ChromeShowcase.kt | 99 +- .../sample/showcase/FieldShowcase.kt | 29 +- .../liquidkit/sample/showcase/MenuShowcase.kt | 73 +- .../sample/showcase/ShowcaseGallery.kt | 193 + .../sample/showcase/StepperMediaShowcase.kt | 46 +- .../sample/showcase/SurfaceShowcase.kt | 108 +- .../liquidkit/sample/MainViewController.kt | 14 +- scripts/publish-module.gradle.kts | 48 - website/.gitignore | 6 + website/components/Hero.jsx | 29 + website/next.config.mjs | 17 + website/package.json | 23 + website/pages/404.mdx | 10 + website/pages/_app.jsx | 6 + website/pages/_meta.jsx | 18 + website/pages/architecture.mdx | 51 + website/pages/components-buttons.mdx | 42 + website/pages/components-fields.mdx | 39 + website/pages/components-menus-pickers.mdx | 39 + website/pages/components-stepper-media.mdx | 38 + website/pages/components-surfaces.mdx | 29 + website/pages/components-toggles-sliders.mdx | 41 + website/pages/components-toolbar-sheet.mdx | 39 + website/pages/getting-started.mdx | 74 + website/pages/index.mdx | 57 + website/pnpm-lock.yaml | 4142 +++++++++++++++++ website/public/favicon.svg | 10 + website/public/robots.txt | 4 + website/styles/globals.css | 141 + website/theme.config.jsx | 106 + 88 files changed, 6998 insertions(+), 1237 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 config/detekt/detekt.yml create mode 100644 sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ShowcaseGallery.kt delete mode 100644 scripts/publish-module.gradle.kts create mode 100644 website/.gitignore create mode 100644 website/components/Hero.jsx create mode 100644 website/next.config.mjs create mode 100644 website/package.json create mode 100644 website/pages/404.mdx create mode 100644 website/pages/_app.jsx create mode 100644 website/pages/_meta.jsx create mode 100644 website/pages/architecture.mdx create mode 100644 website/pages/components-buttons.mdx create mode 100644 website/pages/components-fields.mdx create mode 100644 website/pages/components-menus-pickers.mdx create mode 100644 website/pages/components-stepper-media.mdx create mode 100644 website/pages/components-surfaces.mdx create mode 100644 website/pages/components-toggles-sliders.mdx create mode 100644 website/pages/components-toolbar-sheet.mdx create mode 100644 website/pages/getting-started.mdx create mode 100644 website/pages/index.mdx create mode 100644 website/pnpm-lock.yaml create mode 100644 website/public/favicon.svg create mode 100644 website/public/robots.txt create mode 100644 website/styles/globals.css create mode 100644 website/theme.config.jsx diff --git a/.editorconfig b/.editorconfig index 7d42d1f..b6c7c6b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,13 @@ trim_trailing_whitespace = true [*.{kt,kts}] indent_size = 4 continuation_indent_size = 4 +ktlint_code_style = ktlint_official +max_line_length = 140 +ktlint_standard_max-line-length = disabled +ktlint_standard_function-signature = disabled +# Compose @Composable functions use PascalCase by convention. +ktlint_function_naming_ignore_when_annotated_with = Composable +ktlint_standard_function-naming = disabled [*.{yml,yaml,json,md,xml}] indent_size = 2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2b9517..7e929ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ name: CI on: + push: + branches: [main] + pull_request: workflow_dispatch: concurrency: @@ -8,6 +11,25 @@ concurrency: cancel-in-progress: true jobs: + quality: + name: Quality gates + runs-on: macos-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Spotless + Detekt + Kover + run: ./gradlew spotlessCheck detekt koverXmlReport koverVerify --stacktrace + build: name: Build KMP targets runs-on: macos-latest @@ -32,3 +54,6 @@ jobs: - name: Compile iOS sample run: ./gradlew :sampleApp:compileKotlinIosSimulatorArm64 --stacktrace + + - name: Validate Maven publication + run: ./gradlew publishToMavenLocal --stacktrace diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..0af29a3 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,50 @@ +name: Deploy docs + +on: + push: + branches: [main] + paths: + - 'website/**' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: website + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: website/pnpm-lock.yaml + - run: pnpm install --frozen-lockfile + - run: pnpm build + - uses: actions/upload-pages-artifact@v3 + with: + path: website/out + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 286d1bb..b848ca1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,12 +2,15 @@ name: Publish on: release: - types: [released] + types: [published] workflow_dispatch: +permissions: + contents: read + jobs: publish: - name: Validate release publication + name: Publish to Maven Central runs-on: macos-latest steps: - name: Check out code @@ -22,5 +25,14 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - - name: Build library publications - run: ./gradlew :liquidkit:publishAllPublicationsToLocalBuildRepository :liquidkit-navigation3:publishAllPublicationsToLocalBuildRepository --stacktrace + - name: Verify before publishing + run: ./gradlew spotlessCheck detekt koverXmlReport --stacktrace + + - name: Publish and release to Maven Central + run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache --stacktrace + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} diff --git a/README.md b/README.md index 53e1cc5..fe339dc 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,75 @@ -# LiquidGlass +# LiquidKit -LiquidGlass is a Kotlin Multiplatform component kit for Liquid Glass-style UI. +LiquidKit is a Kotlin Multiplatform component kit for Liquid Glass-style UI. -Android renders with the vendored AndroidLiquidGlass engine. iOS renders with -native UIKit controls through Compose interop. +One shared Compose Multiplatform API; the rendering diverges per platform: -> Maven/module coordinates still use `liquidkit` until a deliberate breaking -> rename is made. +- **Android** renders the glass look in-Compose via an internal backdrop-capture + and refraction (Liquid Glass) shader engine. +- **iOS** wraps **genuine native UIKit controls** (`UISwitch`, `UIButton` glass + configuration, `UIMenu`, `UIPickerView`, `UITextField`, `UISearchTextField`, + `UIStepper`, `UINavigationBar`, `UIVisualEffectView`) so components get the + authentic system Liquid Glass on iOS 26, with a `UIBlurEffect` `systemMaterial` + fallback on earlier versions. + +See [Architecture](#architecture) for details. + +## Install + +LiquidKit is published to Maven Central as `io.github.androidpoet`. ```kotlin -io.github.androidpoet.liquidkit -io.github.androidpoet.liquidkit.navigation3 // optional +// build.gradle.kts (Kotlin Multiplatform) +kotlin { + sourceSets { + commonMain.dependencies { + implementation("io.github.androidpoet:liquidkit:0.1.0") + // Optional Navigation 3 tab-stack helpers: + implementation("io.github.androidpoet:liquidkit-navigation3:0.1.0") + } + } +} +``` + +With a version catalog (`gradle/libs.versions.toml`): + +```toml +[libraries] +liquidkit = { module = "io.github.androidpoet:liquidkit", version = "0.1.0" } +liquidkit-navigation3 = { module = "io.github.androidpoet:liquidkit-navigation3", version = "0.1.0" } ``` +On iOS the library builds a static Kotlin/Native framework named `LiquidKit` +(and `LiquidKitNavigation3` for the optional module); consume it the same way as +any Compose Multiplatform framework embedded by your iOS app. + ## Components -- `LiquidToggle` -- `LiquidSlider` -- `LiquidSegmentedControl` -- `LiquidBottomNavigation` -- `LiquidTabScaffold` +LiquidKit ships an iOS-parity component set: + +- `LiquidButton` (variants via `LiquidButtonVariant`) and `LiquidFab` +- `LiquidSurface` and `LiquidGlassContainer` +- `LiquidDropdownMenu` (`LiquidMenuItem`) and `LiquidPicker` (`LiquidPickerOption`) +- `LiquidTextField` and `LiquidSearchField` +- `LiquidStepper` and `LiquidMediaControl` +- `LiquidToolbar` and `LiquidSheet` +- `LiquidToggle`, `LiquidSlider`, `LiquidSegmentedControl` +- `LiquidBottomNavigation` and `LiquidTabScaffold` - `LiquidNav3TabScaffold` from the optional Navigation 3 module +## Architecture + +LiquidKit is a single cross-platform Compose API backed by `expect`/`actual` +platform implementations: + +- **Android shader** — components render their Liquid Glass appearance with an + internal backdrop capture + refraction shader engine, entirely in Compose. +- **iOS native** — components wrap real UIKit controls through Compose interop, + so they inherit the system's authentic Liquid Glass on iOS 26 (and degrade + gracefully to a `UIBlurEffect` `systemMaterial` on earlier iOS). + +You write one API; each platform draws it the right way. + ## Controls ```kotlin @@ -62,7 +110,7 @@ LiquidBottomNavigation( ) ``` -Use `LiquidTabScaffold` when LiquidGlass should hold the selected tab: +Use `LiquidTabScaffold` when LiquidKit should hold the selected tab: ```kotlin LiquidTabScaffold(items = items) { selectedTab -> @@ -88,7 +136,7 @@ LiquidTabScaffold( ## Navigation 3 Use `liquidkit-navigation3` only for Compose-owned bottom-navigation tab stacks. -LiquidGlass handles the multiple back stacks; your app still owns `NavDisplay`. +LiquidKit handles the multiple back stacks; your app still owns `NavDisplay`. ```kotlin val state = rememberLiquidNav3State( @@ -150,7 +198,7 @@ iosApp/LiquidKitSample.xcodeproj ## Credits -LiquidGlass's Android renderer includes source from +LiquidKit's Android renderer includes source from [Kyant0/AndroidLiquidGlass](https://github.com/Kyant0/AndroidLiquidGlass). The optional Navigation 3 helper is adapted from the multiple-back-stack pattern diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 243cedf..5e5b086 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -6,7 +6,7 @@ LiquidKit vendors Android Liquid Glass renderer and component source from: https://github.com/Kyant0/AndroidLiquidGlass -Original author: Kyant +Original author: Kyant License: Apache License 2.0 The copied files live under: diff --git a/build.gradle.kts b/build.gradle.kts index 2b7c153..e96063f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,7 @@ +import com.diffplug.gradle.spotless.SpotlessExtension import io.github.androidpoet.liquidkit.build.LiquidKitConfiguration +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.extensions.DetektExtension plugins { alias(libs.plugins.android.application) apply false @@ -7,9 +10,115 @@ plugins { alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.compose.multiplatform) apply false alias(libs.plugins.kotlinx.serialization) apply false + alias(libs.plugins.vanniktech.publish) apply false + alias(libs.plugins.spotless) apply false + alias(libs.plugins.detekt) apply false + alias(libs.plugins.dokka) apply false + alias(libs.plugins.kover) } allprojects { group = LiquidKitConfiguration.artifactGroup version = LiquidKitConfiguration.versionName } + +val ktlintVersion = libs.versions.ktlint.get() +val ktlintOverrides = + mapOf( + "ktlint_standard_max-line-length" to "disabled", + "ktlint_standard_function-signature" to "disabled", + // Compose @Composable functions use PascalCase by convention. + "ktlint_standard_function-naming" to "disabled", + ) + +// Root-level Spotless for build scripts and misc files. +apply( + plugin = + libs.plugins.spotless + .get() + .pluginId, +) +configure { + kotlinGradle { + target("*.gradle.kts") + ktlint(ktlintVersion).editorConfigOverride(ktlintOverrides) + } + format("misc") { + target("*.md", ".gitignore", "config/**/*.yml") + trimTrailingWhitespace() + leadingTabsToSpaces() + endWithNewline() + } +} + +subprojects { + apply( + plugin = + rootProject.libs.plugins.spotless + .get() + .pluginId, + ) + apply( + plugin = + rootProject.libs.plugins.detekt + .get() + .pluginId, + ) + + configure { + kotlin { + target("src/**/*.kt") + // The vendored Android Liquid Glass shader engine uses bleeding-edge Kotlin + // (context parameters) that the ktlint engine cannot yet parse; it is kept + // as-is and excluded from formatting/linting. + targetExclude("**/build/**", "**/internal/androidglass/**") + ktlint(ktlintVersion).editorConfigOverride(ktlintOverrides) + trimTrailingWhitespace() + endWithNewline() + } + kotlinGradle { + target("*.gradle.kts") + ktlint(ktlintVersion).editorConfigOverride(ktlintOverrides) + } + } + + configure { + parallel = true + buildUponDefaultConfig = true + config.setFrom(rootProject.files("config/detekt/detekt.yml")) + baseline = file("detekt-baseline.xml") + source.setFrom( + files( + "src/commonMain/kotlin", + "src/commonTest/kotlin", + "src/androidMain/kotlin", + "src/iosMain/kotlin", + "src/appleMain/kotlin", + ).filter { it.exists() }, + ) + } + + tasks.withType().configureEach { + // Use detekt's own embedded analyzer; not all KMP-2.x language features need full type resolution. + jvmTarget = "17" + // The vendored shader engine uses context parameters detekt's parser cannot read. + exclude("**/internal/androidglass/**") + } +} + +dependencies { + kover(project(":liquidkit")) + kover(project(":liquidkit-navigation3")) +} + +kover { + reports { + verify { + // A regression-guard floor for a UI-heavy KMP library that currently ships + // no unit-test suite. Ratchet this up as instrumented/unit tests are added. + rule("Aggregate line coverage") { + minBound(2) + } + } + } +} diff --git a/buildSrc/src/main/kotlin/io/github/androidpoet/liquidkit/build/LiquidKitConfiguration.kt b/buildSrc/src/main/kotlin/io/github/androidpoet/liquidkit/build/LiquidKitConfiguration.kt index 2558469..2793bfc 100644 --- a/buildSrc/src/main/kotlin/io/github/androidpoet/liquidkit/build/LiquidKitConfiguration.kt +++ b/buildSrc/src/main/kotlin/io/github/androidpoet/liquidkit/build/LiquidKitConfiguration.kt @@ -4,6 +4,10 @@ object LiquidKitConfiguration { const val artifactGroup = "io.github.androidpoet" const val versionName = "0.1.0-SNAPSHOT" + // Maven Central artifact ids: io.github.androidpoet:liquidkit and :liquidkit-navigation3 + const val coreArtifactId = "liquidkit" + const val navigation3ArtifactId = "liquidkit-navigation3" + const val compileSdk = 36 const val minSdk = 23 const val targetSdk = 36 diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..82c9394 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,45 @@ +build: + maxIssues: 0 + +complexity: + LongParameterList: + functionThreshold: 12 + constructorThreshold: 14 + ignoreDefaultParameters: true + LongMethod: + threshold: 120 + LargeClass: + excludes: ['**/test/**', '**/*Test.kt', '**/commonTest/**'] + TooManyFunctions: + active: false + CyclomaticComplexMethod: + threshold: 20 + +style: + MagicNumber: + active: false + ReturnCount: + max: 6 + MaxLineLength: + maxLineLength: 150 + UnusedPrivateMember: + active: true + ForbiddenComment: + active: false + +naming: + FunctionNaming: + # Compose composables use PascalCase; iOS interop entry points mirror native names. + ignoreAnnotated: ['Composable'] + active: false + MatchingDeclarationName: + # Several files intentionally group a component with its option/variant types. + active: false + +exceptions: + TooGenericExceptionCaught: + active: false + SwallowedException: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: false diff --git a/gradle.properties b/gradle.properties index 2592b54..1831a38 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,3 +5,22 @@ kotlin.code.style=official org.gradle.caching=true org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8 org.gradle.parallel=true + +# Maven Central (Sonatype Central Portal) publishing metadata. +# Credentials and signing keys are NOT stored here; they are injected at publish +# time via ORG_GRADLE_PROJECT_* environment variables in CI (see publish workflow). +SONATYPE_HOST=CENTRAL_PORTAL +RELEASE_SIGNING_ENABLED=true +POM_NAME=LiquidKit +POM_DESCRIPTION=A Kotlin Multiplatform component kit for Liquid Glass UI. +POM_INCEPTION_YEAR=2026 +POM_URL=https://github.com/AndroidPoet/LiquidKit +POM_LICENSE_NAME=The Apache License, Version 2.0 +POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENSE_DIST=repo +POM_SCM_URL=https://github.com/AndroidPoet/LiquidKit +POM_SCM_CONNECTION=scm:git:git://github.com/AndroidPoet/LiquidKit.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/AndroidPoet/LiquidKit.git +POM_DEVELOPER_ID=androidpoet +POM_DEVELOPER_NAME=Ranbir Singh +POM_DEVELOPER_URL=https://github.com/AndroidPoet diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ddc37a1..f68cf08 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,12 @@ jetbrainsAnnotations = "26.0.2" kyantShapes = "1.2.0" kotlinxCoroutines = "1.10.2" navigation3 = "1.1.1" +vanniktechPublish = "0.30.0" +spotless = "8.6.0" +ktlint = "1.5.0" +detekt = "1.23.7" +kover = "0.8.3" +dokka = "1.9.20" [plugins] android-application = { id = "com.android.application", version.ref = "agp" } @@ -17,6 +23,11 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +vanniktech-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechPublish" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } [libraries] activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } diff --git a/iosApp/LiquidKitSample.xcodeproj/project.pbxproj b/iosApp/LiquidKitSample.xcodeproj/project.pbxproj index c527db1..0602e0d 100644 --- a/iosApp/LiquidKitSample.xcodeproj/project.pbxproj +++ b/iosApp/LiquidKitSample.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd \"$SRCROOT/..\"\ngradle :sampleApp:linkDebugFrameworkIosArm64 :sampleApp:linkDebugFrameworkIosSimulatorArm64\nrm -rf sampleApp/build/XCFrameworks/debug/LiquidKitSample.xcframework\nmkdir -p sampleApp/build/XCFrameworks/debug\nxcodebuild -create-xcframework \\\n -framework sampleApp/build/bin/iosArm64/debugFramework/LiquidKitSample.framework \\\n -framework sampleApp/build/bin/iosSimulatorArm64/debugFramework/LiquidKitSample.framework \\\n -output sampleApp/build/XCFrameworks/debug/LiquidKitSample.xcframework\n"; + shellScript = "cd \"$SRCROOT/..\"\n./gradlew :sampleApp:linkDebugFrameworkIosArm64 :sampleApp:linkDebugFrameworkIosSimulatorArm64\nrm -rf sampleApp/build/XCFrameworks/debug/LiquidKitSample.xcframework\nmkdir -p sampleApp/build/XCFrameworks/debug\nxcodebuild -create-xcframework \\\n -framework sampleApp/build/bin/iosArm64/debugFramework/LiquidKitSample.framework \\\n -framework sampleApp/build/bin/iosSimulatorArm64/debugFramework/LiquidKitSample.framework \\\n -output sampleApp/build/XCFrameworks/debug/LiquidKitSample.xcframework\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/liquidkit-navigation3/build.gradle.kts b/liquidkit-navigation3/build.gradle.kts index 6f1b19b..7852292 100644 --- a/liquidkit-navigation3/build.gradle.kts +++ b/liquidkit-navigation3/build.gradle.kts @@ -8,10 +8,28 @@ plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.multiplatform) alias(libs.plugins.kotlinx.serialization) - id("maven-publish") + alias(libs.plugins.vanniktech.publish) + alias(libs.plugins.dokka) + alias(libs.plugins.kover) } -apply(from = "$rootDir/scripts/publish-module.gradle.kts") +mavenPublishing { + coordinates( + groupId = LiquidKitConfiguration.artifactGroup, + artifactId = LiquidKitConfiguration.navigation3ArtifactId, + version = LiquidKitConfiguration.versionName, + ) + // Sonatype host (CENTRAL_PORTAL) is configured via the SONATYPE_HOST gradle property. + pom { + name.set("LiquidKit Navigation 3") + description.set("Optional Navigation 3 tab-stack helpers for LiquidKit.") + } + if (project.findProperty("RELEASE_SIGNING_ENABLED")?.toString()?.toBoolean() == true && + project.findProperty("signingInMemoryKey") != null + ) { + signAllPublications() + } +} kotlin { androidTarget { diff --git a/liquidkit-navigation3/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3TabScaffold.kt b/liquidkit-navigation3/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3TabScaffold.kt index d335cbf..e0658f7 100644 --- a/liquidkit-navigation3/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3TabScaffold.kt +++ b/liquidkit-navigation3/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3TabScaffold.kt @@ -14,10 +14,6 @@ import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi -import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop -import io.github.androidpoet.liquidkit.internal.captureToLiquidLayerBackdrop -import io.github.androidpoet.liquidkit.internal.rememberLiquidLayerBackdrop import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey @@ -28,6 +24,10 @@ import androidx.navigation3.ui.NavDisplay import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer import androidx.savedstate.serialization.SavedStateConfiguration import io.github.androidpoet.liquidkit.LiquidGlassStyle +import io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi +import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.captureToLiquidLayerBackdrop +import io.github.androidpoet.liquidkit.internal.rememberLiquidLayerBackdrop import io.github.androidpoet.liquidkit.navigation.LiquidBottomNavigation import io.github.androidpoet.liquidkit.navigation.LiquidNavigationItem import kotlinx.serialization.PolymorphicSerializer @@ -49,11 +49,12 @@ public open class LiquidNav3State internal constructor( get() = backStacks[selectedRoute] ?: error("Stack for $selectedRoute not found.") internal val stacksInUse: List - get() = if (selectedRoute == startRoute) { - listOf(startRoute) - } else { - listOf(startRoute, selectedRoute) - } + get() = + if (selectedRoute == startRoute) { + listOf(startRoute) + } else { + listOf(startRoute, selectedRoute) + } public fun backStack(route: NavKey): NavBackStack = backStacks[route] ?: error("Stack for $route not found.") @@ -101,11 +102,11 @@ public class LiquidNav3TabState internal constructor( selectedRoute: MutableState, backStacks: Map>, ) : LiquidNav3State( - startRoute = startRoute, - topLevelRoutes = topLevelRoutes, - selectedRoute = selectedRoute, - backStacks = backStacks, -) + startRoute = startRoute, + topLevelRoutes = topLevelRoutes, + selectedRoute = selectedRoute, + backStacks = backStacks, + ) @Composable public fun rememberLiquidNav3State( @@ -117,17 +118,19 @@ public fun rememberLiquidNav3State( require(startRoute in topLevelRoutes) { "startRoute must be one of the top-level routes." } val rootRoutes = remember(topLevelRoutes) { topLevelRoutes.toSet() } - val selectedRoute = rememberSerializable( - startRoute, - rootRoutes, - configuration = savedStateConfiguration, - serializer = MutableStateSerializer(PolymorphicSerializer(NavKey::class)), - ) { - androidx.compose.runtime.mutableStateOf(startRoute) - } - val backStacks = rootRoutes.associateWith { route -> - rememberNavBackStack(savedStateConfiguration, route) - } + val selectedRoute = + rememberSerializable( + startRoute, + rootRoutes, + configuration = savedStateConfiguration, + serializer = MutableStateSerializer(PolymorphicSerializer(NavKey::class)), + ) { + androidx.compose.runtime.mutableStateOf(startRoute) + } + val backStacks = + rootRoutes.associateWith { route -> + rememberNavBackStack(savedStateConfiguration, route) + } return remember(startRoute, rootRoutes) { LiquidNav3State( @@ -149,17 +152,19 @@ public fun rememberLiquidNav3TabState( require(items.any { it.key == startRoute }) { "startRoute must be one of the tab item keys." } val rootRoutes = remember(items) { items.map { it.key }.toSet() } - val selectedRoute = rememberSerializable( - startRoute, - rootRoutes, - configuration = savedStateConfiguration, - serializer = MutableStateSerializer(PolymorphicSerializer(NavKey::class)), - ) { - androidx.compose.runtime.mutableStateOf(startRoute) - } - val backStacks = rootRoutes.associateWith { route -> - rememberNavBackStack(savedStateConfiguration, route) - } + val selectedRoute = + rememberSerializable( + startRoute, + rootRoutes, + configuration = savedStateConfiguration, + serializer = MutableStateSerializer(PolymorphicSerializer(NavKey::class)), + ) { + androidx.compose.runtime.mutableStateOf(startRoute) + } + val backStacks = + rootRoutes.associateWith { route -> + rememberNavBackStack(savedStateConfiguration, route) + } return remember(startRoute, rootRoutes) { LiquidNav3TabState( @@ -176,13 +181,14 @@ public fun rememberLiquidNav3Entries( state: LiquidNav3State, entryProvider: (NavKey) -> NavEntry, ): SnapshotStateList> { - val decoratedEntries = state.backStacks.mapValues { (_, stack) -> - rememberDecoratedNavEntries( - backStack = stack, - entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator()), - entryProvider = entryProvider, - ) - } + val decoratedEntries = + state.backStacks.mapValues { (_, stack) -> + rememberDecoratedNavEntries( + backStack = stack, + entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator()), + entryProvider = entryProvider, + ) + } return state.stacksInUse .flatMap { route -> decoratedEntries[route] ?: emptyList() } @@ -211,11 +217,12 @@ public fun LiquidNav3TabScaffold( ) }, ) { - val state = rememberLiquidNav3TabState( - items = items, - startRoute = startRoute, - savedStateConfiguration = savedStateConfiguration, - ) + val state = + rememberLiquidNav3TabState( + items = items, + startRoute = startRoute, + savedStateConfiguration = savedStateConfiguration, + ) LiquidNav3TabScaffold( items = items, @@ -277,10 +284,11 @@ public fun BoxScope.LiquidNav3TabContent( modifier: Modifier = Modifier, ) { NavDisplay( - entries = rememberLiquidNav3Entries( - state = state, - entryProvider = entryProvider, - ), + entries = + rememberLiquidNav3Entries( + state = state, + entryProvider = entryProvider, + ), onBack = state::goBack, modifier = modifier, ) diff --git a/liquidkit-navigation3/src/commonTest/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3StateTest.kt b/liquidkit-navigation3/src/commonTest/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3StateTest.kt index f2eafd7..df7455c 100644 --- a/liquidkit-navigation3/src/commonTest/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3StateTest.kt +++ b/liquidkit-navigation3/src/commonTest/kotlin/io/github/androidpoet/liquidkit/navigation3/LiquidNav3StateTest.kt @@ -3,11 +3,11 @@ package io.github.androidpoet.liquidkit.navigation3 import androidx.compose.runtime.mutableStateOf import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -import kotlinx.serialization.Serializable class LiquidNav3StateTest { @Test @@ -74,10 +74,11 @@ class LiquidNav3StateTest { startRoute = HomeRoute, topLevelRoutes = setOf(HomeRoute, SearchRoute), selectedRoute = mutableStateOf(selectedRoute), - backStacks = mapOf( - HomeRoute to NavBackStack(HomeRoute), - SearchRoute to NavBackStack(SearchRoute), - ), + backStacks = + mapOf( + HomeRoute to NavBackStack(HomeRoute), + SearchRoute to NavBackStack(SearchRoute), + ), ) } diff --git a/liquidkit/build.gradle.kts b/liquidkit/build.gradle.kts index 1efd2bb..46a308d 100644 --- a/liquidkit/build.gradle.kts +++ b/liquidkit/build.gradle.kts @@ -7,10 +7,24 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.multiplatform) - id("maven-publish") + alias(libs.plugins.vanniktech.publish) + alias(libs.plugins.dokka) + alias(libs.plugins.kover) } -apply(from = "$rootDir/scripts/publish-module.gradle.kts") +mavenPublishing { + coordinates( + groupId = LiquidKitConfiguration.artifactGroup, + artifactId = LiquidKitConfiguration.coreArtifactId, + version = LiquidKitConfiguration.versionName, + ) + // Sonatype host (CENTRAL_PORTAL) is configured via the SONATYPE_HOST gradle property. + if (project.findProperty("RELEASE_SIGNING_ENABLED")?.toString()?.toBoolean() == true && + project.findProperty("signingInMemoryKey") != null + ) { + signAllPublications() + } +} kotlin { compilerOptions { diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.android.kt index 7b034bb..f08e80e 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.android.kt @@ -5,8 +5,8 @@ package io.github.androidpoet.liquidkit.button import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable -import androidx.compose.ui.draw.alpha import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -44,11 +44,12 @@ internal actual fun PlatformLiquidButton( BasicText( text = text, modifier = Modifier.padding(horizontal = 4.dp), - style = TextStyle( - color = labelColor, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = labelColor, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), ) } } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.android.kt index bd096c0..e8193ee 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.android.kt @@ -5,11 +5,11 @@ package io.github.androidpoet.liquidkit.button import androidx.compose.foundation.Image import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.icon.LiquidIcon diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.android.kt index 782a863..516618e 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.android.kt @@ -2,13 +2,13 @@ package io.github.androidpoet.liquidkit.dropdown +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.spring -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme @@ -45,6 +45,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties +import com.kyant.shapes.Capsule import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop @@ -57,7 +58,6 @@ import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vi import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow -import com.kyant.shapes.Capsule @Composable internal actual fun PlatformLiquidDropdownMenu( @@ -93,15 +93,13 @@ internal actual fun PlatformLiquidDropdownMenu( highlight = { Highlight.Default }, shadow = { Shadow(alpha = 0.12f) }, onDrawSurface = { drawRect(anchorSurface) }, - ) - .clickable( + ).clickable( interactionSource = null, indication = null, enabled = enabled, role = Role.DropdownList, onClick = { expanded = true }, - ) - .height(44.dp) + ).height(44.dp) .padding(horizontal = 18.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), @@ -110,11 +108,12 @@ internal actual fun PlatformLiquidDropdownMenu( text = label, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TextStyle( - color = style.selectedContentColor, - fontSize = 15.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = style.selectedContentColor, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + ), ) BasicText( text = "▾", @@ -179,8 +178,7 @@ private fun DropdownGlassSurface( shadow = { Shadow(alpha = 0.22f) }, innerShadow = { InnerShadow(radius = 6f.dp, alpha = 0.10f) }, onDrawSurface = { drawRect(menuSurface) }, - ) - .padding(6.dp), + ).padding(6.dp), ) { items.forEach { item -> val isSelected = selectedKey != null && item.key == selectedKey @@ -194,8 +192,7 @@ private fun DropdownGlassSurface( enabled = item.enabled, role = Role.Button, onClick = { onSelect(item.key) }, - ) - .alpha(if (item.enabled) 1f else 0.4f) + ).alpha(if (item.enabled) 1f else 0.4f) .padding(horizontal = 12.dp, vertical = 11.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -213,11 +210,12 @@ private fun DropdownGlassSurface( modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TextStyle( - color = contentColor, - fontSize = 15.sp, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, - ), + style = + TextStyle( + color = contentColor, + fontSize = 15.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, + ), ) Spacer(Modifier.width(8.dp)) if (isSelected) { diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.android.kt index 45a199f..a16057b 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.android.kt @@ -3,7 +3,6 @@ package io.github.androidpoet.liquidkit.field import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -56,30 +55,31 @@ internal actual fun PlatformLiquidSearchField( val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop Row( - modifier = modifier - .alpha(if (enabled) 1f else 0.42f) - .fillMaxWidth() - .drawBackdrop( - backdrop = backdrop, - shape = { Capsule() }, - effects = { - vibrancy() - blur(8f.dp.toPx()) - lens(8f.dp.toPx(), 16f.dp.toPx()) - }, - highlight = { Highlight.Default }, - onDrawSurface = { drawRect(style.containerColor) }, - ) - .height(44.dp) - .padding(horizontal = 14.dp), + modifier = + modifier + .alpha(if (enabled) 1f else 0.42f) + .fillMaxWidth() + .drawBackdrop( + backdrop = backdrop, + shape = { Capsule() }, + effects = { + vibrancy() + blur(8f.dp.toPx()) + lens(8f.dp.toPx(), 16f.dp.toPx()) + }, + highlight = { Highlight.Default }, + onDrawSurface = { drawRect(style.containerColor) }, + ).height(44.dp) + .padding(horizontal = 14.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { // Magnifier glyph drawn with Canvas so we need no icon dependency. Box( - modifier = Modifier - .size(16.dp) - .androidMagnifier(style.contentColor), + modifier = + Modifier + .size(16.dp) + .androidMagnifier(style.contentColor), ) Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { @@ -104,16 +104,16 @@ internal actual fun PlatformLiquidSearchField( if (query.isNotEmpty()) { Box( - modifier = Modifier - .size(18.dp) - .clickable( - interactionSource = null, - indication = null, - enabled = enabled, - role = Role.Button, - onClick = { onQueryChange("") }, - ) - .androidClearGlyph(style.contentColor), + modifier = + Modifier + .size(18.dp) + .clickable( + interactionSource = null, + indication = null, + enabled = enabled, + role = Role.Button, + onClick = { onQueryChange("") }, + ).androidClearGlyph(style.contentColor), ) } } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.android.kt index 2400cb5..db963b0 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.android.kt @@ -3,7 +3,6 @@ package io.github.androidpoet.liquidkit.field import androidx.compose.foundation.Image -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -49,8 +48,6 @@ internal actual fun PlatformLiquidTextField( enabled: Boolean, style: LiquidGlassStyle, ) { - val isLight = !isSystemInDarkTheme() - val layerCapture = LocalLiquidLayerBackdrop.current val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop @@ -58,22 +55,22 @@ internal actual fun PlatformLiquidTextField( val fieldShape = RoundedCornerShape(style.cornerRadius) Row( - modifier = modifier - .alpha(if (enabled) 1f else 0.42f) - .fillMaxWidth() - .drawBackdrop( - backdrop = backdrop, - shape = { fieldShape }, - effects = { - vibrancy() - blur(8f.dp.toPx()) - lens(8f.dp.toPx(), 16f.dp.toPx()) - }, - highlight = { Highlight.Default }, - onDrawSurface = { drawRect(style.containerColor) }, - ) - .heightIn(min = 52.dp) - .padding(horizontal = 16.dp, vertical = 12.dp), + modifier = + modifier + .alpha(if (enabled) 1f else 0.42f) + .fillMaxWidth() + .drawBackdrop( + backdrop = backdrop, + shape = { fieldShape }, + effects = { + vibrancy() + blur(8f.dp.toPx()) + lens(8f.dp.toPx(), 16f.dp.toPx()) + }, + highlight = { Highlight.Default }, + onDrawSurface = { drawRect(style.containerColor) }, + ).heightIn(min = 52.dp) + .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt index c057f08..2c94801 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt @@ -24,47 +24,51 @@ import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight. internal fun Modifier.androidLiquidGlass( shape: Shape, style: LiquidGlassStyle, -): Modifier = composed { - // Use a layer backdrop from the composition local if a scaffold has set one up, - // otherwise fall back to an empty canvas backdrop (lens / highlight still apply). - val layerCapture = LocalLiquidLayerBackdrop.current - val emptyBackdrop = rememberCanvasBackdrop {} - val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else emptyBackdrop +): Modifier = + composed { + // Use a layer backdrop from the composition local if a scaffold has set one up, + // otherwise fall back to an empty canvas backdrop (lens / highlight still apply). + val layerCapture = LocalLiquidLayerBackdrop.current + val emptyBackdrop = rememberCanvasBackdrop {} + val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else emptyBackdrop - drawBackdrop( - backdrop = backdrop, - shape = { shape }, - effects = { - vibrancy() - blur(style.androidBlurRadius.toPx()) - lens(style.androidRefractionHeight.toPx(), style.androidRefractionAmount.toPx()) - }, - highlight = { Highlight.Default }, - onDrawSurface = { drawRect(style.containerColor) }, - ) -} + drawBackdrop( + backdrop = backdrop, + shape = { shape }, + effects = { + vibrancy() + blur(style.androidBlurRadius.toPx()) + lens(style.androidRefractionHeight.toPx(), style.androidRefractionAmount.toPx()) + }, + highlight = { Highlight.Default }, + onDrawSurface = { drawRect(style.containerColor) }, + ) + } private val LiquidGlassStyle.androidBlurRadius: Dp - get() = when (this) { - LiquidGlassStyle.Surface -> 16.dp - LiquidGlassStyle.NavigationBar -> 18.dp - LiquidGlassStyle.Control -> 14.dp - else -> (cornerRadius * 0.72f).coerceIn(8.dp, 22.dp) - } + get() = + when (this) { + LiquidGlassStyle.Surface -> 16.dp + LiquidGlassStyle.NavigationBar -> 18.dp + LiquidGlassStyle.Control -> 14.dp + else -> (cornerRadius * 0.72f).coerceIn(8.dp, 22.dp) + } private val LiquidGlassStyle.androidRefractionHeight: Dp - get() = when (this) { - LiquidGlassStyle.Surface -> 10.dp - LiquidGlassStyle.NavigationBar -> 14.dp - LiquidGlassStyle.Control -> 8.dp - else -> (cornerRadius * 0.48f).coerceIn(6.dp, 16.dp) - } + get() = + when (this) { + LiquidGlassStyle.Surface -> 10.dp + LiquidGlassStyle.NavigationBar -> 14.dp + LiquidGlassStyle.Control -> 8.dp + else -> (cornerRadius * 0.48f).coerceIn(6.dp, 16.dp) + } // Refraction amount is roughly 2× height for a pronounced glass-edge distortion. private val LiquidGlassStyle.androidRefractionAmount: Dp - get() = when (this) { - LiquidGlassStyle.Surface -> 20.dp - LiquidGlassStyle.NavigationBar -> 28.dp - LiquidGlassStyle.Control -> 16.dp - else -> (cornerRadius * 0.96f).coerceIn(12.dp, 32.dp) - } + get() = + when (this) { + LiquidGlassStyle.Surface -> 20.dp + LiquidGlassStyle.NavigationBar -> 28.dp + LiquidGlassStyle.Control -> 16.dp + else -> (cornerRadius * 0.96f).coerceIn(12.dp, 32.dp) + } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.android.kt index af4d17e..a819a9b 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.android.kt @@ -25,13 +25,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidBottomTab import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidBottomTabs -import io.github.androidpoet.liquidkit.LiquidGlassStyle @Composable internal actual fun PlatformLiquidBottomNavigation( @@ -53,9 +53,10 @@ internal actual fun PlatformLiquidBottomNavigation( }, backdrop = backdrop, tabsCount = items.size, - modifier = modifier - .fillMaxWidth() - .height(64.dp), + modifier = + modifier + .fillMaxWidth() + .height(64.dp), ) { items.forEach { item -> LiquidBottomTab(onClick = { onSelected(item.key) }) { @@ -102,11 +103,12 @@ private fun LiquidBottomNavigationItem( text = item.label, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TextStyle( - color = contentColor, - fontSize = 12.sp, - fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, - ), + style = + TextStyle( + color = contentColor, + fontSize = 12.sp, + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, + ), ) } } @@ -115,25 +117,28 @@ private fun LiquidBottomNavigationItem( private fun BadgeIndicator(badge: Int, modifier: Modifier = Modifier) { if (badge == 0) { Box( - modifier = modifier - .size(8.dp) - .background(Color(0xFFFF3B30), CircleShape), + modifier = + modifier + .size(8.dp) + .background(Color(0xFFFF3B30), CircleShape), ) } else { val count = badge.coerceAtMost(99) Box( - modifier = modifier - .background(Color(0xFFFF3B30), CircleShape) - .padding(horizontal = 3.dp, vertical = 1.dp), + modifier = + modifier + .background(Color(0xFFFF3B30), CircleShape) + .padding(horizontal = 3.dp, vertical = 1.dp), contentAlignment = Alignment.Center, ) { BasicText( text = count.toString(), - style = TextStyle( - color = Color.White, - fontSize = 9.sp, - fontWeight = FontWeight.Bold, - ), + style = + TextStyle( + color = Color.White, + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + ), ) } } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.android.kt index 061072b..bd8465d 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.android.kt @@ -30,9 +30,9 @@ import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCombinedBackdrop -import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur @@ -78,20 +78,20 @@ internal actual fun PlatformLiquidPicker( ) { // Glass container that the center selection refracts over. LazyColumn( - modifier = Modifier - .layerBackdrop(trackLayer) - .drawBackdrop( - backdrop = outerBackdrop, - shape = { com.kyant.shapes.RoundedRectangle(20.dp) }, - effects = { - vibrancy() - blur(8f.dp.toPx()) - lens(8f.dp.toPx(), 16f.dp.toPx()) - }, - onDrawSurface = { drawRect(containerColor) }, - ) - .fillMaxWidth() - .height(WheelHeight), + modifier = + Modifier + .layerBackdrop(trackLayer) + .drawBackdrop( + backdrop = outerBackdrop, + shape = { com.kyant.shapes.RoundedRectangle(20.dp) }, + effects = { + vibrancy() + blur(8f.dp.toPx()) + lens(8f.dp.toPx(), 16f.dp.toPx()) + }, + onDrawSurface = { drawRect(containerColor) }, + ).fillMaxWidth() + .height(WheelHeight), state = listState, contentPadding = PaddingValues(vertical = RowHeight * 2), ) { @@ -114,12 +114,13 @@ internal actual fun PlatformLiquidPicker( text = option.label, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TextStyle( - color = if (isSelected) style.selectedContentColor else style.contentColor, - fontSize = if (isSelected) 19.sp else 17.sp, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, - textAlign = TextAlign.Center, - ), + style = + TextStyle( + color = if (isSelected) style.selectedContentColor else style.contentColor, + fontSize = if (isSelected) 19.sp else 17.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + textAlign = TextAlign.Center, + ), ) } } @@ -140,8 +141,7 @@ internal actual fun PlatformLiquidPicker( val surfaceColor = if (isLight) Color.Black.copy(0.05f) else Color.White.copy(0.08f) drawRect(surfaceColor) }, - ) - .fillMaxWidth() + ).fillMaxWidth() .height(RowHeight), ) } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.android.kt index 03735ac..dd37090 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.android.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -57,11 +56,12 @@ internal actual fun PlatformLiquidMediaControl( // ▶ / ❚❚ as text glyphs to avoid an icon dependency. BasicText( text = if (isPlaying) "❚❚" else "▶", - style = TextStyle( - color = style.selectedContentColor, - fontSize = if (isPlaying) 16.sp else 20.sp, - fontWeight = FontWeight.Medium, - ), + style = + TextStyle( + color = style.selectedContentColor, + fontSize = if (isPlaying) 16.sp else 20.sp, + fontWeight = FontWeight.Medium, + ), ) } } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.android.kt index b34961d..3c2fbbc 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.android.kt @@ -4,14 +4,17 @@ package io.github.androidpoet.liquidkit.segmented import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.spring +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -20,27 +23,24 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.vector.rememberVectorPainter +import com.kyant.shapes.Capsule import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCombinedBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop -import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens @@ -48,7 +48,6 @@ import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.vi import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.highlight.Highlight import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow -import com.kyant.shapes.Capsule @Composable internal actual fun PlatformLiquidSegmentedControl( @@ -99,8 +98,7 @@ internal actual fun PlatformLiquidSegmentedControl( lens(8f.dp.toPx(), 16f.dp.toPx()) }, onDrawSurface = { drawRect(containerColor) }, - ) - .height(40.dp) + ).height(40.dp) .fillMaxWidth() .padding(3.dp), verticalAlignment = Alignment.CenterVertically, @@ -136,11 +134,12 @@ internal actual fun PlatformLiquidSegmentedControl( text = segment.label, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TextStyle( - color = contentColor, - fontSize = 13.sp, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, - ), + style = + TextStyle( + color = contentColor, + fontSize = 13.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, + ), ) } } @@ -164,8 +163,7 @@ internal actual fun PlatformLiquidSegmentedControl( val surfaceColor = if (isLight) Color.Black.copy(0.06f) else Color.White.copy(0.06f) drawRect(surfaceColor) }, - ) - .height(34.dp) + ).height(34.dp) .fillMaxWidth(1f / segCount), ) } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt index 38e76ed..2d89808 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt @@ -39,10 +39,11 @@ internal actual fun PlatformLiquidSheet( Dialog( onDismissRequest = onDismiss, - properties = DialogProperties( - usePlatformDefaultWidth = false, - dismissOnClickOutside = true, - ), + properties = + DialogProperties( + usePlatformDefaultWidth = false, + dismissOnClickOutside = true, + ), ) { val layerCapture = LocalLiquidLayerBackdrop.current val canvasBackdrop = rememberCanvasBackdrop { drawRect(style.containerColor) } @@ -52,33 +53,35 @@ internal actual fun PlatformLiquidSheet( // Bottom-aligned scrim that fills the dialog window. A tap on the scrim // dismisses; the sheet surface itself sits at the bottom edge. Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.32f)), + modifier = + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.32f)), contentAlignment = Alignment.BottomCenter, ) { - val sheetShape = RoundedCornerShape( - topStart = style.cornerRadius, - topEnd = style.cornerRadius, - bottomStart = 0.dp, - bottomEnd = 0.dp, - ) + val sheetShape = + RoundedCornerShape( + topStart = style.cornerRadius, + topEnd = style.cornerRadius, + bottomStart = 0.dp, + bottomEnd = 0.dp, + ) Box( - modifier = modifier - .fillMaxWidth() - .widthIn(max = 640.dp) - .drawBackdrop( - backdrop = backdrop, - shape = { sheetShape }, - effects = { - vibrancy() - blur(12f.dp.toPx()) - lens(24f.dp.toPx(), 24f.dp.toPx()) - }, - onDrawSurface = { drawRect(style.containerColor) }, - ) - .navigationBarsPadding() - .padding(20.dp), + modifier = + modifier + .fillMaxWidth() + .widthIn(max = 640.dp) + .drawBackdrop( + backdrop = backdrop, + shape = { sheetShape }, + effects = { + vibrancy() + blur(12f.dp.toPx()) + lens(24f.dp.toPx(), 24f.dp.toPx()) + }, + onDrawSurface = { drawRect(style.containerColor) }, + ).navigationBarsPadding() + .padding(20.dp), ) { content() } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.android.kt index 19a9977..595c2a0 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.android.kt @@ -5,11 +5,11 @@ package io.github.androidpoet.liquidkit.slider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop -import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidSlider @Composable diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt index 9b7145a..c5a541c 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt @@ -82,11 +82,12 @@ private fun StepperGlyph(text: String, color: androidx.compose.ui.graphics.Color ) { BasicText( text = text, - style = TextStyle( - color = color, - fontSize = 22.sp, - fontWeight = FontWeight.Medium, - ), + style = + TextStyle( + color = color, + fontSize = 22.sp, + fontWeight = FontWeight.Medium, + ), ) } } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.android.kt index c50fa6f..d32cc23 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.android.kt @@ -20,9 +20,9 @@ import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberLayerBackdrop -import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.layerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.drawBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.blur import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.effects.lens @@ -52,62 +52,64 @@ internal actual fun PlatformLiquidSurface( val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop val animationScope = rememberCoroutineScope() - val interactiveHighlight = remember(animationScope) { - InteractiveHighlight(animationScope = animationScope) - } + val interactiveHighlight = + remember(animationScope) { + InteractiveHighlight(animationScope = animationScope) + } Box( - modifier = modifier - .drawBackdrop( - backdrop = backdrop, - shape = { shape }, - effects = { - vibrancy() - blur(4f.dp.toPx()) - lens(cornerRadius.toPx().coerceAtMost(20f.dp.toPx()), 24f.dp.toPx()) - }, - highlight = { Highlight.Default }, - shadow = { Shadow(alpha = 0.18f) }, - innerShadow = { InnerShadow(radius = 6f.dp, alpha = 0.08f) }, - layerBlock = if (interactive) { - { - val width = size.width - val height = size.height - val progress = interactiveHighlight.pressProgress - val scale = lerp(1f, 1f + 4f.dp.toPx() / size.height, progress) - val maxOffset = size.minDimension - val initialDerivative = 0.05f - val offset = interactiveHighlight.offset - translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset) - translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset) - val maxDragScale = 4f.dp.toPx() / size.height - val offsetAngle = atan2(offset.y, offset.x) - scaleX = scale + maxDragScale * - abs(cos(offsetAngle) * offset.x / size.maxDimension) - scaleY = scale + maxDragScale * - abs(sin(offsetAngle) * offset.y / size.maxDimension) - } - } else { - null - }, - onDrawSurface = { - if (tint.isSpecified) { - drawRect(tint, blendMode = BlendMode.Hue) - drawRect(tint.copy(alpha = 0.55f)) + modifier = + modifier + .drawBackdrop( + backdrop = backdrop, + shape = { shape }, + effects = { + vibrancy() + blur(4f.dp.toPx()) + lens(cornerRadius.toPx().coerceAtMost(20f.dp.toPx()), 24f.dp.toPx()) + }, + highlight = { Highlight.Default }, + shadow = { Shadow(alpha = 0.18f) }, + innerShadow = { InnerShadow(radius = 6f.dp, alpha = 0.08f) }, + layerBlock = + if (interactive) { + { + val width = size.width + val height = size.height + val progress = interactiveHighlight.pressProgress + val scale = lerp(1f, 1f + 4f.dp.toPx() / size.height, progress) + val maxOffset = size.minDimension + val initialDerivative = 0.05f + val offset = interactiveHighlight.offset + translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset) + translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset) + val maxDragScale = 4f.dp.toPx() / size.height + val offsetAngle = atan2(offset.y, offset.x) + scaleX = scale + maxDragScale * + abs(cos(offsetAngle) * offset.x / size.maxDimension) + scaleY = scale + maxDragScale * + abs(sin(offsetAngle) * offset.y / size.maxDimension) + } + } else { + null + }, + onDrawSurface = { + if (tint.isSpecified) { + drawRect(tint, blendMode = BlendMode.Hue) + drawRect(tint.copy(alpha = 0.55f)) + } else { + drawRect(style.containerColor) + } + }, + ).then( + if (interactive) { + Modifier + .then(interactiveHighlight.modifier) + .then(interactiveHighlight.gestureModifier) } else { - drawRect(style.containerColor) - } - }, - ) - .then( - if (interactive) { - Modifier - .then(interactiveHighlight.modifier) - .then(interactiveHighlight.gestureModifier) - } else { - Modifier - }, - ), + Modifier + }, + ), content = content, ) } diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.android.kt index 702eafe..bba0d9b 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.android.kt @@ -5,12 +5,12 @@ package io.github.androidpoet.liquidkit.toggle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.internal.LocalLiquidLayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.Backdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.LayerBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.backdrops.rememberCanvasBackdrop import io.github.androidpoet.liquidkit.internal.androidglass.catalog.components.LiquidToggle -import io.github.androidpoet.liquidkit.LiquidGlassStyle @Composable internal actual fun PlatformLiquidToggle( diff --git a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.android.kt index 3081b6a..49b0d43 100644 --- a/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.android.kt +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.android.kt @@ -37,20 +37,20 @@ internal actual fun PlatformLiquidToolbar( val backdrop: Backdrop = if (layerCapture is LayerBackdrop) layerCapture else canvasBackdrop Box( - modifier = modifier - .fillMaxWidth() - .height(56.dp) - .drawBackdrop( - backdrop = backdrop, - shape = { RoundedCornerShape(style.cornerRadius) }, - effects = { - vibrancy() - blur(8f.dp.toPx()) - lens(20f.dp.toPx(), 20f.dp.toPx()) - }, - onDrawSurface = { drawRect(style.containerColor) }, - ) - .padding(horizontal = 12.dp), + modifier = + modifier + .fillMaxWidth() + .height(56.dp) + .drawBackdrop( + backdrop = backdrop, + shape = { RoundedCornerShape(style.cornerRadius) }, + effects = { + vibrancy() + blur(8f.dp.toPx()) + lens(20f.dp.toPx(), 20f.dp.toPx()) + }, + onDrawSurface = { drawRect(style.containerColor) }, + ).padding(horizontal = 12.dp), contentAlignment = Alignment.Center, ) { // Leading + trailing action slots pinned to the edges. diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyle.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyle.kt index a8848cb..10f9515 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyle.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyle.kt @@ -14,57 +14,62 @@ public data class LiquidGlassStyle( public val selectedContentColor: Color, ) { public companion object { - // ── Light mode ────────────────────────────────────────────────────── - public val Surface: LiquidGlassStyle = LiquidGlassStyle( - cornerRadius = 24.dp, - containerColor = Color.White.copy(alpha = 0.28f), - selectedContainerColor = Color.White.copy(alpha = 0.42f), - contentColor = Color(0xFF4E5968), - selectedContentColor = Color(0xFF101418), - ) + public val Surface: LiquidGlassStyle = + LiquidGlassStyle( + cornerRadius = 24.dp, + containerColor = Color.White.copy(alpha = 0.28f), + selectedContainerColor = Color.White.copy(alpha = 0.42f), + contentColor = Color(0xFF4E5968), + selectedContentColor = Color(0xFF101418), + ) - public val Control: LiquidGlassStyle = LiquidGlassStyle( - cornerRadius = 18.dp, - containerColor = Color.White.copy(alpha = 0.34f), - selectedContainerColor = Color(0xFF111820).copy(alpha = 0.08f), - contentColor = Color(0xFF4E5968), - selectedContentColor = Color(0xFF101418), - ) + public val Control: LiquidGlassStyle = + LiquidGlassStyle( + cornerRadius = 18.dp, + containerColor = Color.White.copy(alpha = 0.34f), + selectedContainerColor = Color(0xFF111820).copy(alpha = 0.08f), + contentColor = Color(0xFF4E5968), + selectedContentColor = Color(0xFF101418), + ) - public val NavigationBar: LiquidGlassStyle = LiquidGlassStyle( - cornerRadius = 28.dp, - containerColor = Color.White.copy(alpha = 0.34f), - selectedContainerColor = Color.White.copy(alpha = 0.42f), - contentColor = Color(0xFF4E5968), - selectedContentColor = Color(0xFF101418), - ) + public val NavigationBar: LiquidGlassStyle = + LiquidGlassStyle( + cornerRadius = 28.dp, + containerColor = Color.White.copy(alpha = 0.34f), + selectedContainerColor = Color.White.copy(alpha = 0.42f), + contentColor = Color(0xFF4E5968), + selectedContentColor = Color(0xFF101418), + ) // ── Dark mode ─────────────────────────────────────────────────────── - public val SurfaceDark: LiquidGlassStyle = LiquidGlassStyle( - cornerRadius = 24.dp, - containerColor = Color.White.copy(alpha = 0.10f), - selectedContainerColor = Color.White.copy(alpha = 0.18f), - contentColor = Color.White.copy(alpha = 0.55f), - selectedContentColor = Color.White.copy(alpha = 0.92f), - ) + public val SurfaceDark: LiquidGlassStyle = + LiquidGlassStyle( + cornerRadius = 24.dp, + containerColor = Color.White.copy(alpha = 0.10f), + selectedContainerColor = Color.White.copy(alpha = 0.18f), + contentColor = Color.White.copy(alpha = 0.55f), + selectedContentColor = Color.White.copy(alpha = 0.92f), + ) - public val ControlDark: LiquidGlassStyle = LiquidGlassStyle( - cornerRadius = 18.dp, - containerColor = Color.White.copy(alpha = 0.12f), - selectedContainerColor = Color.White.copy(alpha = 0.22f), - contentColor = Color.White.copy(alpha = 0.55f), - selectedContentColor = Color.White.copy(alpha = 0.92f), - ) + public val ControlDark: LiquidGlassStyle = + LiquidGlassStyle( + cornerRadius = 18.dp, + containerColor = Color.White.copy(alpha = 0.12f), + selectedContainerColor = Color.White.copy(alpha = 0.22f), + contentColor = Color.White.copy(alpha = 0.55f), + selectedContentColor = Color.White.copy(alpha = 0.92f), + ) - public val NavigationBarDark: LiquidGlassStyle = LiquidGlassStyle( - cornerRadius = 28.dp, - containerColor = Color.White.copy(alpha = 0.10f), - selectedContainerColor = Color.White.copy(alpha = 0.18f), - contentColor = Color.White.copy(alpha = 0.50f), - selectedContentColor = Color.White.copy(alpha = 0.92f), - ) + public val NavigationBarDark: LiquidGlassStyle = + LiquidGlassStyle( + cornerRadius = 28.dp, + containerColor = Color.White.copy(alpha = 0.10f), + selectedContainerColor = Color.White.copy(alpha = 0.18f), + contentColor = Color.White.copy(alpha = 0.50f), + selectedContentColor = Color.White.copy(alpha = 0.92f), + ) } } diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyleBuilder.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyleBuilder.kt index e073fb9..e91b391 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyleBuilder.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyleBuilder.kt @@ -10,10 +10,11 @@ public fun liquidGlassStyle( selectedContainerColor: Color = base.selectedContainerColor, contentColor: Color = base.contentColor, selectedContentColor: Color = base.selectedContentColor, -): LiquidGlassStyle = LiquidGlassStyle( - cornerRadius = cornerRadius, - containerColor = containerColor, - selectedContainerColor = selectedContainerColor, - contentColor = contentColor, - selectedContentColor = selectedContentColor, -) +): LiquidGlassStyle = + LiquidGlassStyle( + cornerRadius = cornerRadius, + containerColor = containerColor, + selectedContainerColor = selectedContainerColor, + contentColor = contentColor, + selectedContentColor = selectedContentColor, + ) diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.kt index eed1815..70c770c 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.kt @@ -69,15 +69,16 @@ public fun LiquidMenu( selectedKey: T? = null, enabled: Boolean = true, style: LiquidGlassStyle = LocalLiquidGlassStyle.current, -): Unit = LiquidDropdownMenu( - items = items, - onSelect = onSelect, - modifier = modifier, - label = label, - selectedKey = selectedKey, - enabled = enabled, - style = style, -) +): Unit = + LiquidDropdownMenu( + items = items, + onSelect = onSelect, + modifier = modifier, + label = label, + selectedKey = selectedKey, + enabled = enabled, + style = style, + ) @Composable internal expect fun PlatformLiquidDropdownMenu( diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidTabScaffold.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidTabScaffold.kt index 238d374..df3b365 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidTabScaffold.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidTabScaffold.kt @@ -18,7 +18,9 @@ import io.github.androidpoet.liquidkit.internal.captureToLiquidLayerBackdrop import io.github.androidpoet.liquidkit.internal.rememberLiquidLayerBackdrop @Stable -public class LiquidTabState internal constructor(initialKey: T) { +public class LiquidTabState internal constructor( + initialKey: T, +) { public var selectedKey: T by mutableStateOf(initialKey) private set @@ -30,9 +32,10 @@ public class LiquidTabState internal constructor(initialKey: T) { @Composable public fun rememberLiquidTabState( initialKey: T, -): LiquidTabState = remember(initialKey) { - LiquidTabState(initialKey) -} +): LiquidTabState = + remember(initialKey) { + LiquidTabState(initialKey) + } @Composable public fun LiquidTabScaffold( diff --git a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.kt index a0b6832..6cd760e 100644 --- a/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.kt +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.kt @@ -2,11 +2,11 @@ package io.github.androidpoet.liquidkit.surface 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.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.unit.Dp import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.LocalLiquidGlassStyle diff --git a/liquidkit/src/commonTest/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyleTest.kt b/liquidkit/src/commonTest/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyleTest.kt index b51b399..b1760c3 100644 --- a/liquidkit/src/commonTest/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyleTest.kt +++ b/liquidkit/src/commonTest/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyleTest.kt @@ -22,11 +22,12 @@ class LiquidGlassStyleTest { @Test fun test_liquidGlassStyle_overridesOnlyProvidedValues() { - val style = liquidGlassStyle( - base = LiquidGlassStyle.NavigationBar, - cornerRadius = 12.dp, - containerColor = Color.Red, - ) + val style = + liquidGlassStyle( + base = LiquidGlassStyle.NavigationBar, + cornerRadius = 12.dp, + containerColor = Color.Red, + ) assertEquals(12.dp, style.cornerRadius) assertEquals(Color.Red, style.containerColor) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.ios.kt index 833bb19..ea2b3f1 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.ios.kt @@ -49,9 +49,10 @@ internal actual fun PlatformLiquidButton( UIKitView( factory = { UIButton().apply { - configuration = glassButtonConfigurationFor( - prominent = variant == LiquidButtonVariant.GlassProminent, - ) + configuration = + glassButtonConfigurationFor( + prominent = variant == LiquidButtonVariant.GlassProminent, + ) setTitle(text, forState = UIControlStateNormal) this.enabled = enabled backgroundColor = UIColor.clearColor @@ -69,10 +70,11 @@ internal actual fun PlatformLiquidButton( button.enabled = enabled }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.ios.kt index 0e6f14a..2b1508e 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.ios.kt @@ -53,9 +53,10 @@ internal actual fun PlatformLiquidFab( UIKitView( factory = { UIButton().apply { - configuration = glassButtonConfigurationFor( - prominent = variant == LiquidButtonVariant.GlassProminent, - ) + configuration = + glassButtonConfigurationFor( + prominent = variant == LiquidButtonVariant.GlassProminent, + ) systemName?.let { name -> setImage(UIImage.systemImageNamed(name), forState = UIControlStateNormal) } @@ -77,10 +78,11 @@ internal actual fun PlatformLiquidFab( button.enabled = enabled }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt index 79b12ae..d5ed476 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt @@ -3,7 +3,6 @@ package io.github.androidpoet.liquidkit.dropdown import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -21,8 +20,8 @@ import platform.UIKit.UIControlStateNormal import platform.UIKit.UIImage import platform.UIKit.UIMenu import platform.UIKit.UIMenuElement -import platform.UIKit.UIMenuElementState import platform.UIKit.UIMenuElementAttributesDisabled +import platform.UIKit.UIMenuElementState /** * Genuine native iOS dropdown: a [UIButton] with `showsMenuAsPrimaryAction = true` @@ -58,18 +57,20 @@ internal actual fun PlatformLiquidDropdownMenu( button.enabled = enabled button.alpha = if (enabled) 1.0 else 0.42 button.setOpaque(false) - button.menu = buildMenu( - items = currentItems.value, - selectedKey = currentSelected.value, - onSelect = { currentOnSelect.value(it) }, - ) + button.menu = + buildMenu( + items = currentItems.value, + selectedKey = currentSelected.value, + onSelect = { currentOnSelect.value(it) }, + ) }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } @@ -79,24 +80,29 @@ private fun buildMenu( selectedKey: T?, onSelect: (T) -> Unit, ): UIMenu { - val actions: List = items.map { item -> - val image: UIImage? = item.icon?.iosSystemNameFor(selectedKey != null && item.key == selectedKey) - ?.let { UIImage.systemImageNamed(it) } - UIAction.actionWithTitle( - title = item.label, - image = image, - identifier = null, - handler = { onSelect(item.key) }, - ).apply { - state = if (selectedKey != null && item.key == selectedKey) { - UIMenuElementState.UIMenuElementStateOn - } else { - UIMenuElementState.UIMenuElementStateOff - } - if (!item.enabled) { - attributes = UIMenuElementAttributesDisabled - } + val actions: List = + items.map { item -> + val image: UIImage? = + item.icon + ?.iosSystemNameFor(selectedKey != null && item.key == selectedKey) + ?.let { UIImage.systemImageNamed(it) } + UIAction + .actionWithTitle( + title = item.label, + image = image, + identifier = null, + handler = { onSelect(item.key) }, + ).apply { + state = + if (selectedKey != null && item.key == selectedKey) { + UIMenuElementState.UIMenuElementStateOn + } else { + UIMenuElementState.UIMenuElementStateOff + } + if (!item.enabled) { + attributes = UIMenuElementAttributesDisabled + } + } } - } return UIMenu.menuWithChildren(actions) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt index b1a5f43..f2aec44 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt @@ -28,7 +28,8 @@ import platform.darwin.NSObject private class LiquidSearchFieldTarget( var onQueryChange: (String) -> Unit, var onSearch: (String) -> Unit, -) : NSObject(), UITextFieldDelegateProtocol { +) : NSObject(), + UITextFieldDelegateProtocol { @ObjCAction fun editingChanged(sender: UITextField) { onQueryChange(sender.text ?: "") @@ -55,12 +56,13 @@ internal actual fun PlatformLiquidSearchField( ) { val currentOnQueryChange = rememberUpdatedState(onQueryChange) val currentOnSearch = rememberUpdatedState(onSearch) - val target = remember { - LiquidSearchFieldTarget( - onQueryChange = { currentOnQueryChange.value(it) }, - onSearch = { currentOnSearch.value(it) }, - ) - } + val target = + remember { + LiquidSearchFieldTarget( + onQueryChange = { currentOnQueryChange.value(it) }, + onSearch = { currentOnSearch.value(it) }, + ) + } target.onQueryChange = { currentOnQueryChange.value(it) } target.onSearch = { currentOnSearch.value(it) } @@ -92,10 +94,11 @@ internal actual fun PlatformLiquidSearchField( field.textColor = style.selectedContentColor.toUIColor() }, onRelease = { it.resignFirstResponder() }, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.ios.kt index cded73d..a2ca108 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.ios.kt @@ -21,8 +21,8 @@ import platform.UIKit.UIColor import platform.UIKit.UIControlEventEditingChanged import platform.UIKit.UIImage import platform.UIKit.UIImageView -import platform.UIKit.UITextField import platform.UIKit.UITextBorderStyle +import platform.UIKit.UITextField import platform.UIKit.UITextFieldViewMode import platform.UIKit.UIView import platform.darwin.NSObject @@ -89,13 +89,14 @@ internal actual fun PlatformLiquidTextField( field.textColor = style.selectedContentColor.toUIColor() }, onRelease = { it.resignFirstResponder() }, - properties = UIKitInteropProperties( - // Cooperative + overlay lets the native field receive taps and become first responder, - // so the system keyboard shows and typing works without manual focus wiring. - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + // Cooperative + overlay lets the native field receive taps and become first responder, + // so the system keyboard shows and typing works without manual focus wiring. + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/IosLiquidGlass.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/IosLiquidGlass.kt index 598a6c9..40f85d8 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/IosLiquidGlass.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/internal/IosLiquidGlass.kt @@ -3,9 +3,10 @@ package io.github.androidpoet.liquidkit.internal import androidx.compose.ui.graphics.Color import platform.UIKit.UIColor -internal fun Color.toUIColor(): UIColor = UIColor( - red = red.toDouble(), - green = green.toDouble(), - blue = blue.toDouble(), - alpha = alpha.toDouble(), -) +internal fun Color.toUIColor(): UIColor = + UIColor( + red = red.toDouble(), + green = green.toDouble(), + blue = blue.toDouble(), + alpha = alpha.toDouble(), + ) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.ios.kt index bf8c586..6860204 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/navigation/LiquidBottomNavigation.ios.kt @@ -25,7 +25,8 @@ import platform.darwin.NSObject private class LiquidTabBarDelegate( private val onItemSelected: (Int) -> Unit, -) : NSObject(), UITabBarDelegateProtocol { +) : NSObject(), + UITabBarDelegateProtocol { override fun tabBar(tabBar: UITabBar, didSelectItem: UITabBarItem) { onItemSelected(didSelectItem.tag.toInt()) } @@ -41,11 +42,12 @@ internal actual fun PlatformLiquidBottomNavigation( style: LiquidGlassStyle, ) { val currentOnSelected = rememberUpdatedState(onSelected) - val delegate = remember { - LiquidTabBarDelegate { index -> - items.getOrNull(index)?.key?.let(currentOnSelected.value) + val delegate = + remember { + LiquidTabBarDelegate { index -> + items.getOrNull(index)?.key?.let(currentOnSelected.value) + } } - } UIKitView( factory = { @@ -54,19 +56,21 @@ internal actual fun PlatformLiquidBottomNavigation( configureLiquidTabBar(items, selectedKey, style) } }, - modifier = modifier - .fillMaxWidth() - .height(83.dp), + modifier = + modifier + .fillMaxWidth() + .height(83.dp), update = { tabBar -> tabBar.delegate = delegate tabBar.configureLiquidTabBar(items, selectedKey, style) }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } @@ -75,22 +79,24 @@ private fun UITabBar.configureLiquidTabBar( selectedKey: T, style: LiquidGlassStyle, ) { - val tabItems = navigationItems.mapIndexed { index, item -> - val selected = item.key == selectedKey - val image = item.icon?.iosSystemNameFor(selected)?.let { UIImage.systemImageNamed(it) } - UITabBarItem( - title = item.label, - image = image, - selectedImage = image, - ).apply { - tag = index.toLong() - badgeValue = when (item.badge) { - null -> null - 0 -> "" - else -> item.badge.coerceAtMost(99).toString() + val tabItems = + navigationItems.mapIndexed { index, item -> + val selected = item.key == selectedKey + val image = item.icon?.iosSystemNameFor(selected)?.let { UIImage.systemImageNamed(it) } + UITabBarItem( + title = item.label, + image = image, + selectedImage = image, + ).apply { + tag = index.toLong() + badgeValue = + when (item.badge) { + null -> null + 0 -> "" + else -> item.badge.coerceAtMost(99).toString() + } } } - } setItems(tabItems, animated = false) selectedItem = tabItems.getOrNull(navigationItems.indexOfFirst { it.key == selectedKey }) diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.ios.kt index 897a41d..e16bea1 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.ios.kt @@ -24,8 +24,9 @@ import platform.darwin.NSObject private class LiquidPickerCoordinator( var titles: List, var onRowSelected: (Int) -> Unit, -) : NSObject(), UIPickerViewDataSourceProtocol, UIPickerViewDelegateProtocol { - +) : NSObject(), + UIPickerViewDataSourceProtocol, + UIPickerViewDelegateProtocol { override fun numberOfComponentsInPickerView(pickerView: UIPickerView): NSInteger = 1L override fun pickerView( @@ -67,17 +68,24 @@ internal actual fun PlatformLiquidPicker( val currentOnSelected = rememberUpdatedState(onSelected) val currentOptions = rememberUpdatedState(options) - val coordinator = remember { - LiquidPickerCoordinator( - titles = options.map { it.label }, - onRowSelected = { index -> - currentOptions.value.getOrNull(index)?.key?.let(currentOnSelected.value) - }, - ) - } + val coordinator = + remember { + LiquidPickerCoordinator( + titles = options.map { it.label }, + onRowSelected = { index -> + currentOptions.value + .getOrNull(index) + ?.key + ?.let(currentOnSelected.value) + }, + ) + } coordinator.titles = options.map { it.label } coordinator.onRowSelected = { index -> - currentOptions.value.getOrNull(index)?.key?.let(currentOnSelected.value) + currentOptions.value + .getOrNull(index) + ?.key + ?.let(currentOnSelected.value) } val selectedIndex = options.indexOfFirst { it.key == selectedKey }.coerceAtLeast(0) @@ -101,10 +109,11 @@ internal actual fun PlatformLiquidPicker( } }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.ios.kt index bea7685..440516f 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.ios.kt @@ -65,14 +65,16 @@ internal actual fun PlatformLiquidMediaControl( val currentOnToggle = rememberUpdatedState(onPlayPauseToggle) val currentIsPlaying = rememberUpdatedState(isPlaying) - val scrubberTarget = remember { - LiquidScrubberTarget { currentOnProgressChange.value(it) } - } + val scrubberTarget = + remember { + LiquidScrubberTarget { currentOnProgressChange.value(it) } + } scrubberTarget.onValueChange = { currentOnProgressChange.value(it) } - val playPauseTarget = remember { - LiquidPlayPauseTarget { currentOnToggle.value(!currentIsPlaying.value) } - } + val playPauseTarget = + remember { + LiquidPlayPauseTarget { currentOnToggle.value(!currentIsPlaying.value) } + } playPauseTarget.onToggle = { currentOnToggle.value(!currentIsPlaying.value) } Row( @@ -104,11 +106,12 @@ internal actual fun PlatformLiquidMediaControl( button.tintColor = style.selectedContentColor.toUIColor() }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) // Genuine native scrubber. @@ -145,11 +148,12 @@ internal actual fun PlatformLiquidMediaControl( slider.thumbTintColor = UIColor.whiteColor }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.ios.kt index 90a6b57..05edf57 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.ios.kt @@ -17,11 +17,10 @@ import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ObjCAction import platform.Foundation.NSSelectorFromString import platform.UIKit.NSForegroundColorAttributeName -import platform.UIKit.UIColor import platform.UIKit.UIControlEventValueChanged import platform.UIKit.UIControlStateNormal -import platform.UIKit.UIImage import platform.UIKit.UIControlStateSelected +import platform.UIKit.UIImage import platform.UIKit.UISegmentedControl import platform.darwin.NSObject @@ -46,11 +45,12 @@ internal actual fun PlatformLiquidSegmentedControl( style: LiquidGlassStyle, ) { val currentOnSelected = rememberUpdatedState(onSelected) - val target = remember { - LiquidSegmentedControlTarget { index -> - segments.getOrNull(index)?.key?.let(currentOnSelected.value) + val target = + remember { + LiquidSegmentedControlTarget { index -> + segments.getOrNull(index)?.key?.let(currentOnSelected.value) + } } - } target.onIndexSelected = { index -> segments.getOrNull(index)?.key?.let(currentOnSelected.value) } @@ -71,11 +71,12 @@ internal actual fun PlatformLiquidSegmentedControl( segmentedControl.configureLiquidSegmentedControl(segments, selectedKey, enabled, style) }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt index 9df7bdb..4831b51 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalForeignApi::class) + package io.github.androidpoet.liquidkit.sheet import androidx.compose.foundation.background @@ -20,16 +22,21 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.UIKitInteropProperties import androidx.compose.ui.viewinterop.UIKitView import io.github.androidpoet.liquidkit.LiquidGlassStyle +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents +import platform.Foundation.NSProcessInfo import platform.UIKit.UIBlurEffect import platform.UIKit.UIBlurEffectStyle +import platform.UIKit.UIGlassEffect +import platform.UIKit.UIGlassEffectStyle import platform.UIKit.UIVisualEffect import platform.UIKit.UIVisualEffectView /** * iOS [LiquidSheet] rendered as a native glass-backed surface using - * [UIVisualEffectView] with a system [UIBlurEffect] (`systemMaterial`). On iOS 26 - * that material already renders as Liquid Glass; the dedicated `UIGlassEffect` - * (not yet in the Kotlin/Native UIKit bindings) is best applied by the SwiftUI host. + * [UIVisualEffectView]. On iOS 26 it uses the dedicated `UIGlassEffect` (the same + * typed binding used by `LiquidSurface`); on earlier systems it falls back to a + * system [UIBlurEffect] (`systemMaterial`). * * NOTE: native sheet *presentation* (`UISheetPresentationController` detents, * grabber, interactive dismiss) is owned by the SwiftUI host. This component is @@ -50,19 +57,21 @@ internal actual fun PlatformLiquidSheet( val scrimInteraction = remember { MutableInteractionSource() } Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.32f)) - .clickable( - interactionSource = scrimInteraction, - indication = null, - ) { currentOnDismiss.value() }, + modifier = + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.32f)) + .clickable( + interactionSource = scrimInteraction, + indication = null, + ) { currentOnDismiss.value() }, contentAlignment = Alignment.BottomCenter, ) { Box( - modifier = modifier - .fillMaxWidth() - .widthIn(max = 640.dp), + modifier = + modifier + .fillMaxWidth() + .widthIn(max = 640.dp), contentAlignment = Alignment.TopCenter, ) { // Native glass background behind the Compose content. @@ -78,19 +87,21 @@ internal actual fun PlatformLiquidSheet( view.layer.cornerRadius = style.cornerRadius.value.toDouble() }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = null, - isNativeAccessibilityEnabled = false, - placedAsOverlay = false, - ), + properties = + UIKitInteropProperties( + interactionMode = null, + isNativeAccessibilityEnabled = false, + placedAsOverlay = false, + ), ) // Compose content on top of the glass. Box( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(20.dp), + modifier = + Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(20.dp), ) { content() } @@ -99,12 +110,18 @@ internal actual fun PlatformLiquidSheet( } /** - * Returns the system glass material for the sheet surface. + * Returns the glass material for the sheet surface. * - * iOS 26's `UIGlassEffect` is not yet exposed in the Kotlin/Native UIKit bindings, - * so this uses a system [UIBlurEffect] (`systemMaterial`) which on iOS 26 already - * renders as the Liquid Glass material; the true `UIGlassEffect` is best applied by - * the SwiftUI host that owns native presentation. See the iOS API gap note. + * On iOS 26 this uses the dedicated `UIGlassEffect` (the same typed binding used by + * `LiquidSurface`); on earlier systems it falls back to a system [UIBlurEffect] + * (`systemMaterial`), which renders as the closest available material. */ -private fun liquidGlassEffect(): UIVisualEffect = - UIBlurEffect.effectWithStyle(UIBlurEffectStyle.UIBlurEffectStyleSystemMaterial) +private fun liquidGlassEffect(): UIVisualEffect { + if (isiOS26OrLater()) { + return UIGlassEffect.effectWithStyle(UIGlassEffectStyle.UIGlassEffectStyleRegular) + } + return UIBlurEffect.effectWithStyle(UIBlurEffectStyle.UIBlurEffectStyleSystemMaterial) +} + +private fun isiOS26OrLater(): Boolean = + NSProcessInfo.processInfo.operatingSystemVersion.useContents { majorVersion >= 26L } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.ios.kt index 85b9ade..311551a 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.ios.kt @@ -42,9 +42,10 @@ internal actual fun PlatformLiquidSlider( style: LiquidGlassStyle, ) { val currentOnValueChange = rememberUpdatedState(onValueChange) - val target = remember { - LiquidSliderTarget { currentOnValueChange.value(it) } - } + val target = + remember { + LiquidSliderTarget { currentOnValueChange.value(it) } + } target.onValueChange = { currentOnValueChange.value(it) } UIKitView( @@ -78,10 +79,11 @@ internal actual fun PlatformLiquidSlider( slider.thumbTintColor = UIColor.whiteColor }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.ios.kt index 3e96d34..10e31a3 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.ios.kt @@ -43,9 +43,10 @@ internal actual fun PlatformLiquidStepper( style: LiquidGlassStyle, ) { val currentOnValueChange = rememberUpdatedState(onValueChange) - val target = remember { - LiquidStepperTarget { currentOnValueChange.value(it) } - } + val target = + remember { + LiquidStepperTarget { currentOnValueChange.value(it) } + } target.onValueChange = { currentOnValueChange.value(it) } UIKitView( @@ -77,10 +78,11 @@ internal actual fun PlatformLiquidStepper( stepper.tintColor = style.selectedContentColor.toUIColor() }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.ios.kt index 682160a..d4b0dd4 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.ios.kt @@ -61,11 +61,12 @@ internal actual fun PlatformLiquidSurface( view.layer.cornerRadius = cornerPx }, onRelease = {}, - properties = UIKitInteropProperties( - // Native glass sits behind Compose content; it must never intercept touches. - interactionMode = null, - isNativeAccessibilityEnabled = false, - ), + properties = + UIKitInteropProperties( + // Native glass sits behind Compose content; it must never intercept touches. + interactionMode = null, + isNativeAccessibilityEnabled = false, + ), ) // Compose content drawn on top of the native glass material. content() diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt index 694ead0..ba47580 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toggle/LiquidToggle.ios.kt @@ -41,9 +41,10 @@ internal actual fun PlatformLiquidToggle( style: LiquidGlassStyle, ) { val currentOnCheckedChange = rememberUpdatedState(onCheckedChange) - val target = remember { - LiquidSwitchTarget { currentOnCheckedChange.value(it) } - } + val target = + remember { + LiquidSwitchTarget { currentOnCheckedChange.value(it) } + } UIKitView( factory = { @@ -69,10 +70,11 @@ internal actual fun PlatformLiquidToggle( uiSwitch.onTintColor = style.selectedContentColor.toUIColor() }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = UIKitInteropInteractionMode.Cooperative(), - isNativeAccessibilityEnabled = true, - placedAsOverlay = true, - ), + properties = + UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(), + isNativeAccessibilityEnabled = true, + placedAsOverlay = true, + ), ) } diff --git a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.ios.kt index fb4fb80..2ce9979 100644 --- a/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.ios.kt +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.ios.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode import androidx.compose.ui.viewinterop.UIKitInteropProperties import androidx.compose.ui.viewinterop.UIKitView import io.github.androidpoet.liquidkit.LiquidGlassStyle @@ -41,9 +40,10 @@ internal actual fun PlatformLiquidToolbar( style: LiquidGlassStyle, ) { Box( - modifier = modifier - .fillMaxWidth() - .height(56.dp), + modifier = + modifier + .fillMaxWidth() + .height(56.dp), ) { // Native glass background. UIKitView( @@ -55,18 +55,20 @@ internal actual fun PlatformLiquidToolbar( modifier = Modifier.fillMaxSize(), update = { bar -> bar.configureLiquidNavigationBar() }, onRelease = {}, - properties = UIKitInteropProperties( - interactionMode = null, - isNativeAccessibilityEnabled = true, - placedAsOverlay = false, - ), + properties = + UIKitInteropProperties( + interactionMode = null, + isNativeAccessibilityEnabled = true, + placedAsOverlay = false, + ), ) // Compose slot overlay. Box( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 12.dp), + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 12.dp), contentAlignment = Alignment.Center, ) { Row( @@ -95,10 +97,11 @@ private fun UINavigationBar.configureLiquidNavigationBar() { // An empty nav item lets the bar render its (glass) chrome without native text, // since the title is supplied by the Compose overlay. setItems(listOf(UINavigationItem(title = "")), animated = false) - val appearance = UINavigationBarAppearance().apply { - // Default background opts the bar into the system material — Liquid Glass on iOS 26. - configureWithDefaultBackground() - } + val appearance = + UINavigationBarAppearance().apply { + // Default background opts the bar into the system material — Liquid Glass on iOS 26. + configureWithDefaultBackground() + } standardAppearance = appearance scrollEdgeAppearance = appearance compactAppearance = appearance diff --git a/sampleApp/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sample/MainActivity.kt b/sampleApp/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sample/MainActivity.kt index fae91bd..d60a70e 100644 --- a/sampleApp/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sample/MainActivity.kt +++ b/sampleApp/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sample/MainActivity.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -69,8 +68,8 @@ private fun GlassCatalogApp() { BackHandler(screen != Screen.Home) { screen = Screen.Home } when (screen) { - Screen.Home -> HomeScreen(onNavigate = { screen = it }) - Screen.Buttons -> ButtonsScreen() + Screen.Home -> HomeScreen(onNavigate = { screen = it }) + Screen.Buttons -> ButtonsScreen() Screen.Controls -> ControlsScreen() Screen.BottomTabs -> BottomTabsScreen() } @@ -81,7 +80,7 @@ private fun GlassCatalogApp() { @Composable private fun DemoScaffold( wallpaper: Int = R.drawable.wallpaper_light, - content: @Composable BoxScope.(backdrop: LayerBackdrop) -> Unit + content: @Composable BoxScope.(backdrop: LayerBackdrop) -> Unit, ) { val backdrop = rememberLayerBackdrop() @@ -89,9 +88,10 @@ private fun DemoScaffold( Image( painter = painterResource(wallpaper), contentDescription = null, - modifier = Modifier - .layerBackdrop(backdrop) - .fillMaxSize(), + modifier = + Modifier + .layerBackdrop(backdrop) + .fillMaxSize(), contentScale = ContentScale.Crop, ) content(backdrop) @@ -125,7 +125,7 @@ private fun HomeScreen(onNavigate: (Screen) -> Unit) { style = TextStyle(fg.copy(0.5f), 14.sp), ) - SectionHeader("Components", fg) + SectionHeader("Components") NavRow("Buttons", fg) { onNavigate(Screen.Buttons) } NavRow("Toggle & Slider", fg) { onNavigate(Screen.Controls) } NavRow("Bottom tabs", fg) { onNavigate(Screen.BottomTabs) } @@ -134,7 +134,7 @@ private fun HomeScreen(onNavigate: (Screen) -> Unit) { } @Composable -private fun SectionHeader(label: String, fg: Color) { +private fun SectionHeader(label: String) { BasicText( label, Modifier.padding(16.dp, 20.dp, 16.dp, 6.dp).fillMaxWidth(), @@ -155,14 +155,20 @@ private fun NavRow(label: String, fg: Color, onClick: () -> Unit) { BasicText(label, style = TextStyle(fg, 17.sp)) BasicText("›", style = TextStyle(fg.copy(0.4f), 20.sp)) } - Box(Modifier.padding(horizontal = 16.dp).fillMaxWidth().height(0.5.dp).background(fg.copy(0.08f))) + Box( + Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .height(0.5.dp) + .background(fg.copy(0.08f)), + ) } // ── Buttons ─────────────────────────────────────────────────────────────────── @Composable private fun ButtonsScreen() { - DemoScaffold { backdrop -> + DemoScaffold { backdrop -> Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), @@ -210,7 +216,14 @@ private fun ControlsScreen() { // Toggle over card (canvas backdrop) var tog2 by rememberSaveable { mutableStateOf(true) } - Box(Modifier.background(cardBg, androidx.compose.foundation.shape.RoundedCornerShape(20.dp)).padding(20.dp)) { + Box( + Modifier + .background( + cardBg, + androidx.compose.foundation.shape + .RoundedCornerShape(20.dp), + ).padding(20.dp), + ) { LiquidToggle({ tog2 }, { tog2 = it }, rememberCanvasBackdrop { drawRect(cardBg) }) } } diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt index 60ebfbd..657cec8 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt @@ -2,7 +2,9 @@ package io.github.androidpoet.liquidkit.sample import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -16,6 +18,7 @@ import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -25,24 +28,25 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.savedstate.serialization.SavedStateConfiguration import io.github.androidpoet.liquidkit.icon.LiquidIcon import io.github.androidpoet.liquidkit.navigation.LiquidNavigationItem import io.github.androidpoet.liquidkit.navigation3.LiquidNav3TabScaffold +import io.github.androidpoet.liquidkit.sample.showcase.ShowcaseEntry +import io.github.androidpoet.liquidkit.sample.showcase.ShowcaseEntryContent import io.github.androidpoet.liquidkit.segmented.LiquidSegment import io.github.androidpoet.liquidkit.segmented.LiquidSegmentedControl import io.github.androidpoet.liquidkit.slider.LiquidSlider import io.github.androidpoet.liquidkit.toggle.LiquidToggle -import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.savedstate.serialization.SavedStateConfiguration import kotlinx.serialization.Serializable import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic @@ -65,55 +69,115 @@ fun LiquidKitSampleApp(modifier: Modifier = Modifier) { @Composable public fun LiquidKitComposeOwnedTabShell(modifier: Modifier = Modifier) { val items = rememberLiquidKitTabItems() - val entryProvider: (NavKey) -> NavEntry = entryProvider { - entry { - LiquidKitSampleTabRoot(selectedTab = LiquidKitSampleTab.Home) + var openShowcase by remember { mutableStateOf(null) } + val onOpenShowcase: (ShowcaseEntry) -> Unit = { openShowcase = it } + + val entryProvider: (NavKey) -> NavEntry = + entryProvider { + entry { + LiquidKitSampleTabRoot( + selectedTab = LiquidKitSampleTab.Home, + onOpenShowcase = onOpenShowcase, + ) + } + entry { + LiquidKitSampleTabRoot( + selectedTab = LiquidKitSampleTab.Search, + onOpenShowcase = onOpenShowcase, + ) + } + entry { + LiquidKitSampleTabRoot( + selectedTab = LiquidKitSampleTab.Settings, + onOpenShowcase = onOpenShowcase, + ) + } } - entry { - LiquidKitSampleTabRoot(selectedTab = LiquidKitSampleTab.Search) - } - entry { - LiquidKitSampleTabRoot(selectedTab = LiquidKitSampleTab.Settings) + + Box(modifier = modifier.fillMaxSize()) { + LiquidNav3TabScaffold( + items = items, + entryProvider = entryProvider, + savedStateConfiguration = liquidKitNav3SavedStateConfiguration, + modifier = + Modifier + .fillMaxSize() + .background(sampleBackground()), + navigationModifier = + Modifier + .navigationBarsPadding() + .padding(16.dp), + ) + + val showcase = openShowcase + if (showcase != null) { + Box(modifier = Modifier.fillMaxSize().background(sampleBackground())) { + ShowcaseEntryContent(showcase, modifier = Modifier.fillMaxSize()) + ShowcaseBackChip( + onClick = { openShowcase = null }, + modifier = + Modifier + .align(Alignment.TopStart) + .safeDrawingPadding() + .padding(16.dp), + ) + } } } +} - LiquidNav3TabScaffold( - items = items, - entryProvider = entryProvider, - savedStateConfiguration = liquidKitNav3SavedStateConfiguration, - modifier = modifier - .fillMaxSize() - .background(sampleBackground()), - navigationModifier = Modifier - .navigationBarsPadding() - .padding(16.dp), - ) +@Composable +private fun ShowcaseBackChip( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = + modifier + .background(Color(0xCC0A0A0A), RoundedCornerShape(20.dp)) + .border(1.dp, Color(0x33FFFFFF), RoundedCornerShape(20.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 18.dp, vertical = 10.dp), + ) { + BasicText( + text = "‹ Back", + style = + TextStyle( + color = Color.White, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + } } @Composable public fun LiquidKitSampleTabContent( selectedTab: LiquidKitSampleTab, modifier: Modifier = Modifier, + onOpenShowcase: (ShowcaseEntry) -> Unit = {}, extraContent: @Composable ColumnScope.() -> Unit = {}, ) { Column( - modifier = modifier - .fillMaxSize() - .safeDrawingPadding() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 20.dp, vertical = 30.dp), + modifier = + modifier + .fillMaxSize() + .safeDrawingPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 30.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(24.dp), ) { Header( selectedTab = selectedTab, - modifier = Modifier - .fillMaxWidth() - .widthIn(max = 620.dp), + modifier = + Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), ) when (selectedTab) { - LiquidKitSampleTab.Home -> HomeSamplePanel() + LiquidKitSampleTab.Home -> HomeSamplePanel(onOpenShowcase = onOpenShowcase) LiquidKitSampleTab.Search -> ControlsSamplePanel() LiquidKitSampleTab.Settings -> SettingsSamplePanel() } @@ -128,41 +192,47 @@ public fun LiquidKitSampleTabContent( public fun LiquidKitSampleTabRoot( selectedTab: LiquidKitSampleTab, modifier: Modifier = Modifier, + onOpenShowcase: (ShowcaseEntry) -> Unit = {}, ) { LiquidKitSampleTabContent( selectedTab = selectedTab, - modifier = modifier - .fillMaxSize() - .background(sampleBackground()), + onOpenShowcase = onOpenShowcase, + modifier = + modifier + .fillMaxSize() + .background(sampleBackground()), ) } @Composable -private fun HomeSamplePanel() { +private fun HomeSamplePanel(onOpenShowcase: (ShowcaseEntry) -> Unit = {}) { var enabled by remember { mutableStateOf(true) } var intensity by remember { mutableStateOf(0.58f) } InvertedPanel( - modifier = Modifier - .fillMaxWidth() - .widthIn(max = 620.dp), + modifier = + Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), ) { BasicText( text = "One API. Native ownership.", - style = TextStyle( - color = Color.White, - fontSize = 23.sp, - fontWeight = FontWeight.Bold, - lineHeight = 28.sp, - ), + style = + TextStyle( + color = Color.White, + fontSize = 23.sp, + fontWeight = FontWeight.Bold, + lineHeight = 28.sp, + ), ) BasicText( text = "Android renders Kyant-style liquid controls. iOS keeps native tab ownership.", - style = TextStyle( - color = Color(0xFFD7D7D7), - fontSize = 14.sp, - lineHeight = 20.sp, - ), + style = + TextStyle( + color = Color(0xFFD7D7D7), + fontSize = 14.sp, + lineHeight = 20.sp, + ), ) Spacer(modifier = Modifier.height(10.dp)) InvertedMetricRow(label = "Tab layer", value = "Nav3") @@ -170,9 +240,28 @@ private fun HomeSamplePanel() { } ComponentPanel( - modifier = Modifier - .fillMaxWidth() - .widthIn(max = 620.dp), + modifier = + Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), + ) { + PanelSectionTitle( + title = "Component gallery", + description = + "Open any group full-screen. Each is a self-contained showcase " + + "(Android shader glass, iOS native controls).", + ) + ShowcaseEntry.entries.forEach { entry -> + PanelDivider() + ShowcaseGalleryRow(entry = entry, onClick = { onOpenShowcase(entry) }) + } + } + + ComponentPanel( + modifier = + Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), ) { PanelSectionTitle( title = "Home controls", @@ -207,9 +296,10 @@ private fun ControlsSamplePanel() { val densitySegments = rememberControlDensitySegments() ComponentPanel( - modifier = Modifier - .fillMaxWidth() - .widthIn(max = 620.dp), + modifier = + Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), ) { PanelSectionTitle( title = "Controls lab", @@ -248,9 +338,10 @@ private fun SettingsSamplePanel() { val densitySegments = rememberControlDensitySegments() ComponentPanel( - modifier = Modifier - .fillMaxWidth() - .widthIn(max = 620.dp), + modifier = + Modifier + .fillMaxWidth() + .widthIn(max = 620.dp), ) { PanelSectionTitle( title = "Library settings", @@ -292,15 +383,17 @@ private data object LiquidKitSearchRoute : NavKey @Serializable private data object LiquidKitSettingsRoute : NavKey -private val liquidKitNav3SavedStateConfiguration = SavedStateConfiguration { - serializersModule = SerializersModule { - polymorphic(NavKey::class) { - subclass(LiquidKitHomeRoute::class, LiquidKitHomeRoute.serializer()) - subclass(LiquidKitSearchRoute::class, LiquidKitSearchRoute.serializer()) - subclass(LiquidKitSettingsRoute::class, LiquidKitSettingsRoute.serializer()) - } +private val liquidKitNav3SavedStateConfiguration = + SavedStateConfiguration { + serializersModule = + SerializersModule { + polymorphic(NavKey::class) { + subclass(LiquidKitHomeRoute::class, LiquidKitHomeRoute.serializer()) + subclass(LiquidKitSearchRoute::class, LiquidKitSearchRoute.serializer()) + subclass(LiquidKitSettingsRoute::class, LiquidKitSettingsRoute.serializer()) + } + } } -} @Composable private fun rememberLiquidKitTabItems(): List> = @@ -309,31 +402,34 @@ private fun rememberLiquidKitTabItems(): List> = LiquidNavigationItem( key = LiquidKitHomeRoute, label = LiquidKitSampleTab.Home.title, - icon = LiquidIcon( - imageVector = SampleIcons.Home, - contentDescription = LiquidKitSampleTab.Home.title, - iosSystemName = LiquidKitSampleTab.Home.iosSystemImage, - selectedIosSystemName = "house.fill", - ), + icon = + LiquidIcon( + imageVector = SampleIcons.Home, + contentDescription = LiquidKitSampleTab.Home.title, + iosSystemName = LiquidKitSampleTab.Home.iosSystemImage, + selectedIosSystemName = "house.fill", + ), ), LiquidNavigationItem( key = LiquidKitSearchRoute, label = LiquidKitSampleTab.Search.title, - icon = LiquidIcon( - imageVector = SampleIcons.Controls, - contentDescription = LiquidKitSampleTab.Search.title, - iosSystemName = LiquidKitSampleTab.Search.iosSystemImage, - ), + icon = + LiquidIcon( + imageVector = SampleIcons.Controls, + contentDescription = LiquidKitSampleTab.Search.title, + iosSystemName = LiquidKitSampleTab.Search.iosSystemImage, + ), ), LiquidNavigationItem( key = LiquidKitSettingsRoute, label = LiquidKitSampleTab.Settings.title, - icon = LiquidIcon( - imageVector = SampleIcons.Menu, - contentDescription = LiquidKitSampleTab.Settings.title, - iosSystemName = LiquidKitSampleTab.Settings.iosSystemImage, - selectedIosSystemName = "gearshape.fill", - ), + icon = + LiquidIcon( + imageVector = SampleIcons.Menu, + contentDescription = LiquidKitSampleTab.Settings.title, + iosSystemName = LiquidKitSampleTab.Settings.iosSystemImage, + selectedIosSystemName = "gearshape.fill", + ), ), ) } @@ -343,16 +439,18 @@ private fun Header( selectedTab: LiquidKitSampleTab, modifier: Modifier = Modifier, ) { - val title = when (selectedTab) { - LiquidKitSampleTab.Home -> "LiquidKit Home" - LiquidKitSampleTab.Search -> "Controls Lab" - LiquidKitSampleTab.Settings -> "Settings" - } - val subtitle = when (selectedTab) { - LiquidKitSampleTab.Home -> "Bottom navigation with a single moving liquid selection." - LiquidKitSampleTab.Search -> "Android controls rendered by vendored AndroidLiquidGlass." - LiquidKitSampleTab.Settings -> "Platform ownership stays simple: SwiftUI tabs on iOS, Compose tabs on Android." - } + val title = + when (selectedTab) { + LiquidKitSampleTab.Home -> "LiquidKit Home" + LiquidKitSampleTab.Search -> "Controls Lab" + LiquidKitSampleTab.Settings -> "Settings" + } + val subtitle = + when (selectedTab) { + LiquidKitSampleTab.Home -> "Bottom navigation with a single moving liquid selection." + LiquidKitSampleTab.Search -> "Android controls rendered by vendored AndroidLiquidGlass." + LiquidKitSampleTab.Settings -> "Platform ownership stays simple: SwiftUI tabs on iOS, Compose tabs on Android." + } Column( modifier = modifier, @@ -360,20 +458,22 @@ private fun Header( ) { BasicText( text = title, - style = TextStyle( - color = Color(0xFF070707), - fontSize = 34.sp, - fontWeight = FontWeight.Bold, - lineHeight = 38.sp, - ), + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 34.sp, + fontWeight = FontWeight.Bold, + lineHeight = 38.sp, + ), ) BasicText( text = subtitle, - style = TextStyle( - color = Color(0xFF4A4A4A), - fontSize = 15.sp, - lineHeight = 21.sp, - ), + style = + TextStyle( + color = Color(0xFF4A4A4A), + fontSize = 15.sp, + lineHeight = 21.sp, + ), ) } } @@ -384,17 +484,16 @@ private fun InvertedPanel( content: @Composable ColumnScope.() -> Unit, ) { Column( - modifier = modifier - .background( - color = Color(0xFF080808), - shape = RoundedCornerShape(32.dp), - ) - .border( - width = 1.dp, - color = Color(0xFF2A2A2A), - shape = RoundedCornerShape(32.dp), - ) - .padding(22.dp), + modifier = + modifier + .background( + color = Color(0xFF080808), + shape = RoundedCornerShape(32.dp), + ).border( + width = 1.dp, + color = Color(0xFF2A2A2A), + shape = RoundedCornerShape(32.dp), + ).padding(22.dp), verticalArrangement = Arrangement.spacedBy(12.dp), content = content, ) @@ -413,18 +512,20 @@ private fun InvertedMetricRow( BasicText( text = label, modifier = Modifier.weight(1f), - style = TextStyle( - color = Color(0xFFAFAFAF), - fontSize = 14.sp, - ), + style = + TextStyle( + color = Color(0xFFAFAFAF), + fontSize = 14.sp, + ), ) BasicText( text = value, - style = TextStyle( - color = Color.White, - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ), ) } } @@ -447,17 +548,16 @@ private fun ComponentPanel( content: @Composable ColumnScope.() -> Unit, ) { Column( - modifier = modifier - .background( - color = Color(0xF7FFFFFF), - shape = RoundedCornerShape(28.dp), - ) - .border( - width = 1.dp, - color = Color(0x1F000000), - shape = RoundedCornerShape(28.dp), - ) - .padding(horizontal = 18.dp, vertical = 8.dp), + modifier = + modifier + .background( + color = Color(0xF7FFFFFF), + shape = RoundedCornerShape(28.dp), + ).border( + width = 1.dp, + color = Color(0x1F000000), + shape = RoundedCornerShape(28.dp), + ).padding(horizontal = 18.dp, vertical = 8.dp), content = content, ) } @@ -468,28 +568,62 @@ private fun StatusRow( value: String, ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 14.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 14.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { BasicText( text = label, modifier = Modifier.weight(1f), - style = TextStyle( - color = Color(0xFF505050), - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - ), + style = + TextStyle( + color = Color(0xFF505050), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ), ) BasicText( text = value, - style = TextStyle( - color = Color(0xFF070707), - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + } +} + +@Composable +private fun ShowcaseGalleryRow( + entry: ShowcaseEntry, + onClick: () -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LabelBlock( + label = entry.title, + description = entry.summary, + modifier = Modifier.weight(1f), + ) + BasicText( + text = "Open", + style = + TextStyle( + color = Color(0xFF1B6BF2), + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ), ) } } @@ -497,10 +631,11 @@ private fun StatusRow( @Composable private fun PanelDivider() { Spacer( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .background(Color(0x1F000000)), + modifier = + Modifier + .fillMaxWidth() + .height(1.dp) + .background(Color(0x1F000000)), ) } @@ -531,9 +666,10 @@ private fun LiquidKitToggleRow( onValueChange: (Boolean) -> Unit, ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 14.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 14.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -558,9 +694,10 @@ private fun LiquidKitSliderRow( onValueChange: (Float) -> Unit, ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Row( @@ -575,11 +712,12 @@ private fun LiquidKitSliderRow( ) BasicText( text = "${(value * 100).toInt()}%", - style = TextStyle( - color = Color(0xFF070707), - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ), ) } LiquidSlider( @@ -599,9 +737,10 @@ private fun LiquidKitSegmentedRow( onSelected: (ControlDensity) -> Unit, ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { LabelBlock( @@ -629,27 +768,31 @@ private fun LabelBlock( ) { BasicText( text = label, - style = TextStyle( - color = Color(0xFF070707), - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), ) BasicText( text = description, - style = TextStyle( - color = Color(0xFF555555), - fontSize = 13.sp, - lineHeight = 18.sp, - ), + style = + TextStyle( + color = Color(0xFF555555), + fontSize = 13.sp, + lineHeight = 18.sp, + ), ) } } -private fun sampleBackground(): Brush = Brush.linearGradient( - colors = listOf( - Color(0xFFF7F7F7), - Color(0xFFEDEDED), - Color(0xFFDADADA), - ), -) +private fun sampleBackground(): Brush = + Brush.linearGradient( + colors = + listOf( + Color(0xFFF7F7F7), + Color(0xFFEDEDED), + Color(0xFFDADADA), + ), + ) diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/SampleIcons.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/SampleIcons.kt index 34c4f81..94f0799 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/SampleIcons.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/SampleIcons.kt @@ -10,152 +10,157 @@ import androidx.compose.ui.unit.dp internal object SampleIcons { val Home: ImageVector by lazy { - ImageVector.Builder( - name = "Home", - defaultWidth = 24.dp, - defaultHeight = 24.dp, - viewportWidth = 24f, - viewportHeight = 24f, - ).apply { - path( - fill = null, - stroke = SolidColor(Color.Black), - strokeLineWidth = 1.8f, - strokeLineCap = StrokeCap.Round, - strokeLineJoin = StrokeJoin.Round, - ) { - moveTo(4f, 11f) - lineTo(12f, 4f) - lineTo(20f, 11f) - lineTo(20f, 20f) - lineTo(15f, 20f) - lineTo(15f, 14f) - lineTo(9f, 14f) - lineTo(9f, 20f) - lineTo(4f, 20f) - close() - } - }.build() + ImageVector + .Builder( + name = "Home", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = null, + stroke = SolidColor(Color.Black), + strokeLineWidth = 1.8f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + ) { + moveTo(4f, 11f) + lineTo(12f, 4f) + lineTo(20f, 11f) + lineTo(20f, 20f) + lineTo(15f, 20f) + lineTo(15f, 14f) + lineTo(9f, 14f) + lineTo(9f, 20f) + lineTo(4f, 20f) + close() + } + }.build() } val Controls: ImageVector by lazy { - ImageVector.Builder( - name = "Controls", - defaultWidth = 24.dp, - defaultHeight = 24.dp, - viewportWidth = 24f, - viewportHeight = 24f, - ).apply { - path( - fill = null, - stroke = SolidColor(Color.Black), - strokeLineWidth = 1.8f, - strokeLineCap = StrokeCap.Round, - strokeLineJoin = StrokeJoin.Round, - ) { - moveTo(5f, 7f) - lineTo(19f, 7f) - moveTo(8f, 7f) - lineTo(8f, 7f) - moveTo(5f, 12f) - lineTo(19f, 12f) - moveTo(16f, 12f) - lineTo(16f, 12f) - moveTo(5f, 17f) - lineTo(19f, 17f) - moveTo(11f, 17f) - lineTo(11f, 17f) - } - }.build() + ImageVector + .Builder( + name = "Controls", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = null, + stroke = SolidColor(Color.Black), + strokeLineWidth = 1.8f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + ) { + moveTo(5f, 7f) + lineTo(19f, 7f) + moveTo(8f, 7f) + lineTo(8f, 7f) + moveTo(5f, 12f) + lineTo(19f, 12f) + moveTo(16f, 12f) + lineTo(16f, 12f) + moveTo(5f, 17f) + lineTo(19f, 17f) + moveTo(11f, 17f) + lineTo(11f, 17f) + } + }.build() } val Menu: ImageVector by lazy { - ImageVector.Builder( - name = "Menu", - defaultWidth = 24.dp, - defaultHeight = 24.dp, - viewportWidth = 24f, - viewportHeight = 24f, - ).apply { - path( - fill = null, - stroke = SolidColor(Color.Black), - strokeLineWidth = 1.9f, - strokeLineCap = StrokeCap.Round, - strokeLineJoin = StrokeJoin.Round, - ) { - moveTo(5f, 7f) - lineTo(19f, 7f) - moveTo(5f, 12f) - lineTo(19f, 12f) - moveTo(5f, 17f) - lineTo(19f, 17f) - } - }.build() + ImageVector + .Builder( + name = "Menu", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = null, + stroke = SolidColor(Color.Black), + strokeLineWidth = 1.9f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + ) { + moveTo(5f, 7f) + lineTo(19f, 7f) + moveTo(5f, 12f) + lineTo(19f, 12f) + moveTo(5f, 17f) + lineTo(19f, 17f) + } + }.build() } val Sun: ImageVector by lazy { - ImageVector.Builder( - name = "Sun", - defaultWidth = 24.dp, - defaultHeight = 24.dp, - viewportWidth = 24f, - viewportHeight = 24f, - ).apply { - path( - fill = null, - stroke = SolidColor(Color.Black), - strokeLineWidth = 1.8f, - strokeLineCap = StrokeCap.Round, - strokeLineJoin = StrokeJoin.Round, - ) { - moveTo(12f, 5f) - lineTo(12f, 3.5f) - moveTo(12f, 20.5f) - lineTo(12f, 19f) - moveTo(5f, 12f) - lineTo(3.5f, 12f) - moveTo(20.5f, 12f) - lineTo(19f, 12f) - moveTo(7.05f, 7.05f) - lineTo(6f, 6f) - moveTo(18f, 18f) - lineTo(16.95f, 16.95f) - moveTo(16.95f, 7.05f) - lineTo(18f, 6f) - moveTo(6f, 18f) - lineTo(7.05f, 16.95f) - moveTo(15.5f, 12f) - arcTo(3.5f, 3.5f, 0f, true, true, 8.5f, 12f) - arcTo(3.5f, 3.5f, 0f, true, true, 15.5f, 12f) - } - }.build() + ImageVector + .Builder( + name = "Sun", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = null, + stroke = SolidColor(Color.Black), + strokeLineWidth = 1.8f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + ) { + moveTo(12f, 5f) + lineTo(12f, 3.5f) + moveTo(12f, 20.5f) + lineTo(12f, 19f) + moveTo(5f, 12f) + lineTo(3.5f, 12f) + moveTo(20.5f, 12f) + lineTo(19f, 12f) + moveTo(7.05f, 7.05f) + lineTo(6f, 6f) + moveTo(18f, 18f) + lineTo(16.95f, 16.95f) + moveTo(16.95f, 7.05f) + lineTo(18f, 6f) + moveTo(6f, 18f) + lineTo(7.05f, 16.95f) + moveTo(15.5f, 12f) + arcTo(3.5f, 3.5f, 0f, true, true, 8.5f, 12f) + arcTo(3.5f, 3.5f, 0f, true, true, 15.5f, 12f) + } + }.build() } val Moon: ImageVector by lazy { - ImageVector.Builder( - name = "Moon", - defaultWidth = 24.dp, - defaultHeight = 24.dp, - viewportWidth = 24f, - viewportHeight = 24f, - ).apply { - path( - fill = null, - stroke = SolidColor(Color.Black), - strokeLineWidth = 1.8f, - strokeLineCap = StrokeCap.Round, - strokeLineJoin = StrokeJoin.Round, - ) { - moveTo(18.5f, 15.5f) - curveTo(16.9f, 16.7f, 14.8f, 17.2f, 12.8f, 16.7f) - curveTo(8.9f, 15.7f, 6.5f, 11.8f, 7.5f, 7.9f) - curveTo(7.8f, 6.8f, 8.3f, 5.8f, 9f, 5f) - curveTo(6.2f, 6f, 4.2f, 8.7f, 4.2f, 11.9f) - curveTo(4.2f, 16.2f, 7.8f, 19.8f, 12.1f, 19.8f) - curveTo(15f, 19.8f, 17.5f, 18.1f, 18.5f, 15.5f) - close() - } - }.build() + ImageVector + .Builder( + name = "Moon", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = null, + stroke = SolidColor(Color.Black), + strokeLineWidth = 1.8f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + ) { + moveTo(18.5f, 15.5f) + curveTo(16.9f, 16.7f, 14.8f, 17.2f, 12.8f, 16.7f) + curveTo(8.9f, 15.7f, 6.5f, 11.8f, 7.5f, 7.9f) + curveTo(7.8f, 6.8f, 8.3f, 5.8f, 9f, 5f) + curveTo(6.2f, 6f, 4.2f, 8.7f, 4.2f, 11.9f) + curveTo(4.2f, 16.2f, 7.8f, 19.8f, 12.1f, 19.8f) + curveTo(15f, 19.8f, 17.5f, 18.1f, 18.5f, 15.5f) + close() + } + }.build() } } diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ButtonShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ButtonShowcase.kt index ecdaf79..e103b27 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ButtonShowcase.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ButtonShowcase.kt @@ -14,9 +14,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -27,29 +27,30 @@ import io.github.androidpoet.liquidkit.button.LiquidFab import io.github.androidpoet.liquidkit.icon.LiquidIcon private val PlusIcon: ImageVector by lazy { - ImageVector.Builder( - name = "Plus", - defaultWidth = 24.dp, - defaultHeight = 24.dp, - viewportWidth = 24f, - viewportHeight = 24f, - ).apply { - path(fill = SolidColor(Color.White)) { - moveTo(11f, 5f) - horizontalLineTo(13f) - verticalLineTo(11f) - horizontalLineTo(19f) - verticalLineTo(13f) - horizontalLineTo(13f) - verticalLineTo(19f) - horizontalLineTo(11f) - verticalLineTo(13f) - horizontalLineTo(5f) - verticalLineTo(11f) - horizontalLineTo(11f) - close() - } - }.build() + ImageVector + .Builder( + name = "Plus", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(Color.White)) { + moveTo(11f, 5f) + horizontalLineTo(13f) + verticalLineTo(11f) + horizontalLineTo(19f) + verticalLineTo(13f) + horizontalLineTo(13f) + verticalLineTo(19f) + horizontalLineTo(11f) + verticalLineTo(13f) + horizontalLineTo(5f) + verticalLineTo(11f) + horizontalLineTo(11f) + close() + } + }.build() } /** @@ -61,27 +62,30 @@ private val PlusIcon: ImageVector by lazy { public fun ButtonShowcase() { var clicks by remember { mutableStateOf(0) } - val fabIcon = remember { - LiquidIcon( - imageVector = PlusIcon, - contentDescription = "Add", - iosSystemName = "plus", - ) - } + val fabIcon = + remember { + LiquidIcon( + imageVector = PlusIcon, + contentDescription = "Add", + iosSystemName = "plus", + ) + } Column( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(24.dp), verticalArrangement = Arrangement.spacedBy(20.dp), ) { BasicText( text = "Liquid Glass Buttons", - style = TextStyle( - color = Color(0xFF101418), - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - ), + style = + TextStyle( + color = Color(0xFF101418), + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ), ) BasicText( diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ChromeShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ChromeShowcase.kt index 06527dd..1ccd69e 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ChromeShowcase.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ChromeShowcase.kt @@ -42,30 +42,33 @@ public fun ChromeShowcase(modifier: Modifier = Modifier) { var sheetVisible by remember { mutableStateOf(false) } Box( - modifier = modifier - .fillMaxSize() - .background( - Brush.verticalGradient( - listOf(Color(0xFF6D83F2), Color(0xFFA66BF0), Color(0xFFF26D9C)), + modifier = + modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + listOf(Color(0xFF6D83F2), Color(0xFFA66BF0), Color(0xFFF26D9C)), + ), ), - ), ) { Column( - modifier = Modifier - .fillMaxSize() - .safeDrawingPadding() - .padding(16.dp), + modifier = + Modifier + .fillMaxSize() + .safeDrawingPadding() + .padding(16.dp), verticalArrangement = Arrangement.spacedBy(20.dp), ) { LiquidToolbar( title = { BasicText( text = "Liquid Chrome", - style = TextStyle( - color = Color(0xFF101418), - fontSize = 17.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color(0xFF101418), + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + ), ) }, leading = { @@ -79,11 +82,12 @@ public fun ChromeShowcase(modifier: Modifier = Modifier) { BasicText( text = "Done", modifier = Modifier.clickable { /* no-op */ }, - style = TextStyle( - color = Color(0xFF0A84FF), - fontSize = 15.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color(0xFF0A84FF), + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + ), ) }, ) @@ -92,20 +96,22 @@ public fun ChromeShowcase(modifier: Modifier = Modifier) { // "Open sheet" pill. Box( - modifier = Modifier - .fillMaxWidth() - .background(Color.White.copy(alpha = 0.38f), RoundedCornerShape(22.dp)) - .clickable { sheetVisible = true } - .padding(vertical = 14.dp), + modifier = + Modifier + .fillMaxWidth() + .background(Color.White.copy(alpha = 0.38f), RoundedCornerShape(22.dp)) + .clickable { sheetVisible = true } + .padding(vertical = 14.dp), contentAlignment = Alignment.Center, ) { BasicText( text = "Open glass sheet", - style = TextStyle( - color = Color(0xFF101418), - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color(0xFF101418), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), ) } } @@ -118,31 +124,34 @@ public fun ChromeShowcase(modifier: Modifier = Modifier) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { BasicText( text = "Liquid Glass Sheet", - style = TextStyle( - color = Color(0xFF101418), - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - ), + style = + TextStyle( + color = Color(0xFF101418), + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ), ) BasicText( text = "A glass-backed bottom sheet. Tap the scrim to dismiss.", style = TextStyle(color = Color(0xFF4E5968), fontSize = 14.sp), ) Box( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFF0A84FF), RoundedCornerShape(20.dp)) - .clickable { sheetVisible = false } - .padding(vertical = 12.dp), + modifier = + Modifier + .fillMaxWidth() + .background(Color(0xFF0A84FF), RoundedCornerShape(20.dp)) + .clickable { sheetVisible = false } + .padding(vertical = 12.dp), contentAlignment = Alignment.Center, ) { BasicText( text = "Dismiss", - style = TextStyle( - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color.White, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), ) } } diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/FieldShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/FieldShowcase.kt index f628572..a761253 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/FieldShowcase.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/FieldShowcase.kt @@ -39,15 +39,15 @@ public fun FieldShowcase(modifier: Modifier = Modifier) { var lastSearch by remember { mutableStateOf("") } Column( - modifier = modifier - .fillMaxSize() - .background( - Brush.verticalGradient( - listOf(Color(0xFF1B3A6B), Color(0xFF6B3A8C), Color(0xFFB0507A)), - ), - ) - .verticalScroll(rememberScrollState()) - .padding(20.dp), + modifier = + modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + listOf(Color(0xFF1B3A6B), Color(0xFF6B3A8C), Color(0xFFB0507A)), + ), + ).verticalScroll(rememberScrollState()) + .padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { SectionTitle("Liquid Glass Fields") @@ -102,10 +102,11 @@ private fun SectionTitle(text: String) { private fun SectionLabel(text: String) { BasicText( text = text, - style = TextStyle( - color = Color.White.copy(alpha = 0.7f), - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - ), + style = + TextStyle( + color = Color.White.copy(alpha = 0.7f), + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + ), ) } diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/MenuShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/MenuShowcase.kt index 35f7bf5..aff75eb 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/MenuShowcase.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/MenuShowcase.kt @@ -28,14 +28,18 @@ import io.github.androidpoet.liquidkit.dropdown.LiquidMenuItem import io.github.androidpoet.liquidkit.picker.LiquidPicker import io.github.androidpoet.liquidkit.picker.LiquidPickerOption -private enum class SortOrder(val label: String) { +private enum class SortOrder( + val label: String, +) { Newest("Newest"), Oldest("Oldest"), Name("Name (A-Z)"), Size("Size"), } -private enum class TimeZone(val label: String) { +private enum class TimeZone( + val label: String, +) { Pacific("Pacific Time"), Mountain("Mountain Time"), Central("Central Time"), @@ -53,20 +57,23 @@ public fun MenuShowcase(modifier: Modifier = Modifier) { var sort by remember { mutableStateOf(SortOrder.Newest) } var zone by remember { mutableStateOf(TimeZone.Pacific) } - val sortItems = remember { - SortOrder.entries.map { LiquidMenuItem(key = it, label = it.label) } - } - val zoneOptions = remember { - TimeZone.entries.map { LiquidPickerOption(key = it, label = it.label) } - } + val sortItems = + remember { + SortOrder.entries.map { LiquidMenuItem(key = it, label = it.label) } + } + val zoneOptions = + remember { + TimeZone.entries.map { LiquidPickerOption(key = it, label = it.label) } + } Column( - modifier = modifier - .fillMaxSize() - .background(showcaseBackground()) - .safeDrawingPadding() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 20.dp, vertical = 28.dp), + modifier = + modifier + .fillMaxSize() + .background(showcaseBackground()) + .safeDrawingPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 28.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(28.dp), ) { @@ -104,11 +111,12 @@ private fun ShowcaseTitle(title: String, subtitle: String) { ) { BasicText( text = title, - style = TextStyle( - color = Color(0xFF070707), - fontSize = 32.sp, - fontWeight = FontWeight.Bold, - ), + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + ), ) BasicText( text = subtitle, @@ -129,11 +137,12 @@ private fun ShowcaseSection( ) { BasicText( text = title, - style = TextStyle( - color = Color(0xFF070707), - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + ), ) content() BasicText( @@ -143,10 +152,12 @@ private fun ShowcaseSection( } } -private fun showcaseBackground(): Brush = Brush.linearGradient( - colors = listOf( - Color(0xFFF7F7F7), - Color(0xFFEDEDED), - Color(0xFFDADADA), - ), -) +private fun showcaseBackground(): Brush = + Brush.linearGradient( + colors = + listOf( + Color(0xFFF7F7F7), + Color(0xFFEDEDED), + Color(0xFFDADADA), + ), + ) diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ShowcaseGallery.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ShowcaseGallery.kt new file mode 100644 index 0000000..f3e2fe9 --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ShowcaseGallery.kt @@ -0,0 +1,193 @@ +package io.github.androidpoet.liquidkit.sample.showcase + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * The full set of self-contained component showcases, identified for navigation. + * + * Each entry maps a human-readable title/summary to the showcase composable that + * renders that component group. Android renders the Liquid Glass shader engine; + * iOS wraps genuine native UIKit controls. + */ +public enum class ShowcaseEntry( + public val title: String, + public val summary: String, +) { + Button("Buttons & FAB", "LiquidButton variants and LiquidFab"), + Surface("Surfaces", "LiquidSurface and LiquidGlassContainer"), + Menu("Menus & Pickers", "LiquidDropdownMenu and LiquidPicker"), + Field("Fields", "LiquidTextField and LiquidSearchField"), + StepperMedia("Stepper & Media", "LiquidStepper and LiquidMediaControl"), + Chrome("Toolbar & Sheet", "LiquidToolbar and LiquidSheet"), +} + +/** Renders the showcase composable for the given [entry]. */ +@Composable +public fun ShowcaseEntryContent(entry: ShowcaseEntry, modifier: Modifier = Modifier) { + when (entry) { + ShowcaseEntry.Button -> Box(modifier.fillMaxSize()) { ButtonShowcase() } + ShowcaseEntry.Surface -> SurfaceShowcase(modifier) + ShowcaseEntry.Menu -> MenuShowcase(modifier) + ShowcaseEntry.Field -> FieldShowcase(modifier) + ShowcaseEntry.StepperMedia -> Box(modifier.fillMaxSize()) { StepperMediaShowcase() } + ShowcaseEntry.Chrome -> ChromeShowcase(modifier) + } +} + +/** + * A self-contained gallery that lists every component showcase and opens the + * selected one full-screen with a back affordance. Drop it into any host; it owns + * its own selection state and demonstrates the entire LiquidKit component set. + */ +@Composable +public fun ShowcaseGallery(modifier: Modifier = Modifier) { + var selected by remember { mutableStateOf(null) } + val current = selected + + if (current == null) { + ShowcaseGalleryList( + modifier = modifier, + onSelect = { selected = it }, + ) + } else { + Box(modifier = modifier.fillMaxSize()) { + ShowcaseEntryContent(current, modifier = Modifier.fillMaxSize()) + BackChip( + onClick = { selected = null }, + modifier = + Modifier + .align(Alignment.TopStart) + .safeDrawingPadding() + .padding(16.dp), + ) + } + } +} + +@Composable +private fun ShowcaseGalleryList( + onSelect: (ShowcaseEntry) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + BasicText( + text = "Component gallery", + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + ), + ) + BasicText( + text = "Open any component group full-screen. Every entry is a self-contained showcase.", + style = + TextStyle( + color = Color(0xFF555555), + fontSize = 13.sp, + lineHeight = 18.sp, + ), + ) + ShowcaseEntry.entries.forEach { entry -> + GalleryRow(entry = entry, onClick = { onSelect(entry) }) + } + } +} + +@Composable +private fun GalleryRow( + entry: ShowcaseEntry, + onClick: () -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .background(Color(0xFFF3F3F3), RoundedCornerShape(18.dp)) + .border(1.dp, Color(0x14000000), RoundedCornerShape(18.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + BasicText( + text = entry.title, + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + BasicText( + text = entry.summary, + style = TextStyle(color = Color(0xFF666666), fontSize = 13.sp), + ) + } + BasicText( + text = "Open", + style = + TextStyle( + color = Color(0xFF1B6BF2), + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + } +} + +@Composable +private fun BackChip( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = + modifier + .background(Color(0xCC0A0A0A), RoundedCornerShape(20.dp)) + .border(1.dp, Color(0x33FFFFFF), RoundedCornerShape(20.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 18.dp, vertical = 10.dp), + ) { + BasicText( + text = "‹ Back", + style = + TextStyle( + color = Color.White, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + ), + ) + } +} diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/StepperMediaShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/StepperMediaShowcase.kt index 5cffbde..da10f81 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/StepperMediaShowcase.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/StepperMediaShowcase.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -22,7 +23,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.foundation.text.BasicText import io.github.androidpoet.liquidkit.LiquidGlassStyle import io.github.androidpoet.liquidkit.LiquidKitTheme import io.github.androidpoet.liquidkit.play.LiquidMediaControl @@ -38,19 +38,19 @@ import io.github.androidpoet.liquidkit.stepper.LiquidStepper public fun StepperMediaShowcase() { LiquidKitTheme(style = LiquidGlassStyle.SurfaceDark) { Column( - modifier = Modifier - .fillMaxSize() - .background( - Brush.verticalGradient( - listOf( - Color(0xFF1E2A78), - Color(0xFF6A1B9A), - Color(0xFFFF6F00), + modifier = + Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + listOf( + Color(0xFF1E2A78), + Color(0xFF6A1B9A), + Color(0xFFFF6F00), + ), ), - ), - ) - .safeDrawingPadding() - .padding(24.dp), + ).safeDrawingPadding() + .padding(24.dp), verticalArrangement = Arrangement.spacedBy(32.dp), ) { SectionLabel("Liquid Stepper") @@ -93,11 +93,12 @@ public fun StepperMediaShowcase() { private fun SectionLabel(text: String) { BasicText( text = text, - style = TextStyle( - color = Color.White, - fontSize = 20.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color.White, + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + ), ) } @@ -105,9 +106,10 @@ private fun SectionLabel(text: String) { private fun ValueLabel(text: String) { BasicText( text = text, - style = TextStyle( - color = Color.White.copy(alpha = 0.85f), - fontSize = 15.sp, - ), + style = + TextStyle( + color = Color.White.copy(alpha = 0.85f), + fontSize = 15.sp, + ), ) } diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/SurfaceShowcase.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/SurfaceShowcase.kt index 5b8f84f..ff38b3c 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/SurfaceShowcase.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/SurfaceShowcase.kt @@ -37,30 +37,33 @@ public fun SurfaceShowcase(modifier: Modifier = Modifier) { var interactive by remember { mutableStateOf(true) } Column( - modifier = modifier - .fillMaxSize() - .background(showcaseBackground()) - .safeDrawingPadding() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 20.dp, vertical = 28.dp), + modifier = + modifier + .fillMaxSize() + .background(showcaseBackground()) + .safeDrawingPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 28.dp), verticalArrangement = Arrangement.spacedBy(20.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { BasicText( text = "Liquid Surface", modifier = Modifier.fillMaxWidth(), - style = TextStyle( - color = Color.White, - fontSize = 30.sp, - fontWeight = FontWeight.Bold, - ), + style = + TextStyle( + color = Color.White, + fontSize = 30.sp, + fontWeight = FontWeight.Bold, + ), ) // A simple glass card with content on top. LiquidSurface( - modifier = Modifier - .fillMaxWidth() - .height(140.dp), + modifier = + Modifier + .fillMaxWidth() + .height(140.dp), ) { CardLabel( title = "Default card", @@ -70,9 +73,10 @@ public fun SurfaceShowcase(modifier: Modifier = Modifier) { // Tinted, interactive glass card. LiquidSurface( - modifier = Modifier - .fillMaxWidth() - .height(140.dp), + modifier = + Modifier + .fillMaxWidth() + .height(140.dp), shape = RoundedCornerShape(36.dp), tint = Color(0xFF4FC3F7), interactive = interactive, @@ -85,24 +89,27 @@ public fun SurfaceShowcase(modifier: Modifier = Modifier) { // A container grouping two glass children that share one sampling region (Android). LiquidGlassContainer( - modifier = Modifier - .fillMaxWidth() - .height(160.dp), + modifier = + Modifier + .fillMaxWidth() + .height(160.dp), ) { LiquidSurface( - modifier = Modifier - .fillMaxWidth(0.5f) - .height(120.dp) - .align(Alignment.CenterStart), + modifier = + Modifier + .fillMaxWidth(0.5f) + .height(120.dp) + .align(Alignment.CenterStart), tint = Color(0xFFFFB74D), ) { CardLabel(title = "Grouped A", subtitle = "Shared region") } LiquidSurface( - modifier = Modifier - .fillMaxWidth(0.5f) - .height(120.dp) - .align(Alignment.CenterEnd), + modifier = + Modifier + .fillMaxWidth(0.5f) + .height(120.dp) + .align(Alignment.CenterEnd), tint = Color(0xFFBA68C8), ) { CardLabel(title = "Grouped B", subtitle = "Shared region") @@ -117,34 +124,39 @@ private fun CardLabel( subtitle: String, ) { Column( - modifier = Modifier - .fillMaxSize() - .padding(18.dp), + modifier = + Modifier + .fillMaxSize() + .padding(18.dp), verticalArrangement = Arrangement.Center, ) { BasicText( text = title, - style = TextStyle( - color = Color.White, - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + ), ) BasicText( text = subtitle, - style = TextStyle( - color = Color.White.copy(alpha = 0.85f), - fontSize = 13.sp, - ), + style = + TextStyle( + color = Color.White.copy(alpha = 0.85f), + fontSize = 13.sp, + ), ) } } -private fun showcaseBackground(): Brush = Brush.linearGradient( - colors = listOf( - Color(0xFF1A237E), - Color(0xFF6A1B9A), - Color(0xFFAD1457), - Color(0xFFEF6C00), - ), -) +private fun showcaseBackground(): Brush = + Brush.linearGradient( + colors = + listOf( + Color(0xFF1A237E), + Color(0xFF6A1B9A), + Color(0xFFAD1457), + Color(0xFFEF6C00), + ), + ) diff --git a/sampleApp/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sample/MainViewController.kt b/sampleApp/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sample/MainViewController.kt index 33561bd..c24df81 100644 --- a/sampleApp/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sample/MainViewController.kt +++ b/sampleApp/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sample/MainViewController.kt @@ -3,9 +3,10 @@ package io.github.androidpoet.liquidkit.sample import androidx.compose.ui.window.ComposeUIViewController import platform.UIKit.UIViewController -fun MainViewController(): UIViewController = ComposeUIViewController { - LiquidKitComposeOwnedTabShell() -} +fun MainViewController(): UIViewController = + ComposeUIViewController { + LiquidKitComposeOwnedTabShell() + } fun HomeViewController(): UIViewController = tabRootViewController(LiquidKitSampleTab.Home) @@ -13,6 +14,7 @@ fun SearchViewController(): UIViewController = tabRootViewController(LiquidKitSa fun SettingsViewController(): UIViewController = tabRootViewController(LiquidKitSampleTab.Settings) -private fun tabRootViewController(tab: LiquidKitSampleTab): UIViewController = ComposeUIViewController { - LiquidKitSampleTabRoot(selectedTab = tab) -} +private fun tabRootViewController(tab: LiquidKitSampleTab): UIViewController = + ComposeUIViewController { + LiquidKitSampleTabRoot(selectedTab = tab) + } diff --git a/scripts/publish-module.gradle.kts b/scripts/publish-module.gradle.kts deleted file mode 100644 index b22eb99..0000000 --- a/scripts/publish-module.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication - -val publicationName = when (project.name) { - "liquidkit-navigation3" -> "LiquidKit Navigation 3" - else -> "LiquidKit" -} -val publicationDescription = when (project.name) { - "liquidkit-navigation3" -> "Optional Navigation 3 tab-stack helpers for LiquidKit." - else -> "A Kotlin Multiplatform component kit for Liquid Glass UI." -} - -configure { - publications.withType().configureEach { - pom { - name.set(publicationName) - description.set(publicationDescription) - url.set("https://github.com/AndroidPoet/LiquidKit") - - licenses { - license { - name.set("The Apache License, Version 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - - developers { - developer { - id.set("AndroidPoet") - name.set("AndroidPoet") - } - } - - scm { - connection.set("scm:git:git://github.com/AndroidPoet/LiquidKit.git") - developerConnection.set("scm:git:ssh://github.com/AndroidPoet/LiquidKit.git") - url.set("https://github.com/AndroidPoet/LiquidKit") - } - } - } - - repositories { - maven { - name = "localBuild" - url = uri(layout.buildDirectory.dir("repo").get().asFile) - } - } -} diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000..ab342a7 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,6 @@ +node_modules +.next +out +.DS_Store +*.log +.vercel diff --git a/website/components/Hero.jsx b/website/components/Hero.jsx new file mode 100644 index 0000000..900d676 --- /dev/null +++ b/website/components/Hero.jsx @@ -0,0 +1,29 @@ +export function Hero() { + return ( +
+ Kotlin Multiplatform +

LiquidKit

+

+ Liquid Glass UI for Compose Multiplatform — Android shader rendering, genuine native iOS controls. +

+ +
+ ) +} + +export default Hero diff --git a/website/next.config.mjs b/website/next.config.mjs new file mode 100644 index 0000000..1dc5401 --- /dev/null +++ b/website/next.config.mjs @@ -0,0 +1,17 @@ +import nextra from 'nextra' + +const withNextra = nextra({ + theme: 'nextra-theme-docs', + themeConfig: './theme.config.jsx', + defaultShowCopyCode: true, +}) + +const basePath = '/LiquidKit' + +export default withNextra({ + output: 'export', + images: { unoptimized: true }, + reactStrictMode: true, + basePath, + assetPrefix: basePath, +}) diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..9be63ff --- /dev/null +++ b/website/package.json @@ -0,0 +1,23 @@ +{ + "name": "liquidkit-docs", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^15.5.18", + "nextra": "^3.3.1", + "nextra-theme-docs": "^3.3.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "pnpm": { + "overrides": { + "postcss": "^8.5.10" + } + } +} diff --git a/website/pages/404.mdx b/website/pages/404.mdx new file mode 100644 index 0000000..057e927 --- /dev/null +++ b/website/pages/404.mdx @@ -0,0 +1,10 @@ +--- +title: Page not found +description: The page you were looking for does not exist. +--- + +# 404 — Page not found + +The page you were looking for does not exist. + +[Back to the LiquidKit docs →](/) diff --git a/website/pages/_app.jsx b/website/pages/_app.jsx new file mode 100644 index 0000000..98ff64b --- /dev/null +++ b/website/pages/_app.jsx @@ -0,0 +1,6 @@ +import 'nextra-theme-docs/style.css' +import '../styles/globals.css' + +export default function App({ Component, pageProps }) { + return +} diff --git a/website/pages/_meta.jsx b/website/pages/_meta.jsx new file mode 100644 index 0000000..52c88c9 --- /dev/null +++ b/website/pages/_meta.jsx @@ -0,0 +1,18 @@ +export default { + index: 'Introduction', + 'getting-started': 'Getting Started', + architecture: 'Architecture', + 'components-buttons': 'Buttons & FAB', + 'components-surfaces': 'Surfaces', + 'components-menus-pickers': 'Menus & Pickers', + 'components-fields': 'Fields', + 'components-stepper-media': 'Stepper & Media', + 'components-toolbar-sheet': 'Toolbar & Sheet', + 'components-toggles-sliders': 'Toggles, Sliders & Segmented', + github_link: { + title: 'GitHub', + type: 'page', + href: 'https://github.com/AndroidPoet/LiquidKit', + newWindow: true, + }, +} diff --git a/website/pages/architecture.mdx b/website/pages/architecture.mdx new file mode 100644 index 0000000..bcd5268 --- /dev/null +++ b/website/pages/architecture.mdx @@ -0,0 +1,51 @@ +--- +title: Architecture +description: How LiquidKit renders Liquid Glass — an Android shader engine and genuine native iOS controls behind one shared Compose API. +--- + +import { Callout } from 'nextra/components' + +# Architecture + +LiquidKit exposes **one shared Compose Multiplatform API** built with `expect`/`actual`. Every +component is declared once in `commonMain`; the platform `actual` implementations diverge so each +platform renders the glass the way that platform does it best. + +## Android: shader rendering + +On Android, LiquidKit renders the Liquid Glass look entirely in-Compose. An internal **backdrop +capture + refraction / Liquid Glass shader engine** samples what is drawn behind a component and +applies the glass distortion, highlight and tint. On supported API levels this uses a +`RuntimeShader` (AGSL) pipeline; the engine is internal to the library, so host apps just place the +composables and get the effect without managing any platform views. + +## iOS: genuine native controls + +On iOS, each component wraps a **real UIKit control** rather than re-drawing it. Because the +controls are genuine system views, on **iOS 26** they automatically adopt the system Liquid Glass +material; on earlier versions they fall back to a `UIBlurEffect` `systemMaterial`. + + +Because iOS uses real UIKit controls, accessibility, focus, system gestures and the platform look +all come for free — and they track future OS updates automatically. + + +## Component → renderer mapping + +| Component | Android renderer | iOS native control | +| --- | --- | --- | +| `LiquidButton` / `LiquidFab` | Shader glass surface | `UIButton` glass configuration | +| `LiquidSurface` / `LiquidGlassContainer` | Shader glass surface | `UIVisualEffectView` (`UIGlassEffect` / `UIBlurEffect`) | +| `LiquidDropdownMenu` | Shader glass surface | `UIButton` + `UIMenu` | +| `LiquidPicker` | Shader glass surface | `UIPickerView` | +| `LiquidTextField` | Shader glass surface | `UITextField` | +| `LiquidSearchField` | Shader glass surface | `UISearchTextField` | +| `LiquidStepper` | Shader glass surface | `UIStepper` | +| `LiquidMediaControl` | Shader glass surface | Native transport controls | +| `LiquidToolbar` | Shader glass surface | `UINavigationBar` | +| `LiquidSheet` | Shader glass surface | Native glass surface (`UIVisualEffectView`) | +| `LiquidToggle` | Shader glass surface | `UISwitch` | +| `LiquidSlider` | Shader glass surface | `UISlider` | +| `LiquidSegmentedControl` | Shader glass surface | `UISegmentedControl` | + +The takeaway: **Android shader, iOS native** — one API, two faithful renderings. diff --git a/website/pages/components-buttons.mdx b/website/pages/components-buttons.mdx new file mode 100644 index 0000000..abf0e68 --- /dev/null +++ b/website/pages/components-buttons.mdx @@ -0,0 +1,42 @@ +--- +title: Buttons & FAB +description: LiquidButton and LiquidFab — Liquid Glass buttons that render as native UIButton glass on iOS. +--- + +# Buttons & FAB + +`LiquidButton` is a Liquid Glass button with style variants via `LiquidButtonVariant`, and +`LiquidFab` is the floating action button. + +```kotlin +import io.github.androidpoet.liquidkit.button.LiquidButton +import io.github.androidpoet.liquidkit.button.LiquidButtonVariant +import io.github.androidpoet.liquidkit.button.LiquidFab + +@Composable +fun Buttons() { + LiquidButton( + onClick = { /* … */ }, + variant = LiquidButtonVariant.Filled, + ) { + Text("Primary") + } + + LiquidButton( + onClick = { /* … */ }, + variant = LiquidButtonVariant.Tinted, + ) { + Text("Secondary") + } + + LiquidFab(onClick = { /* … */ }) { + LiquidIcon(/* + icon */) + } +} +``` + +## Android vs iOS + +- **Android** — the button is drawn by the internal Liquid Glass shader engine. +- **iOS** — wraps a native `UIButton` with its glass configuration, so it picks up the system + Liquid Glass material on iOS 26. diff --git a/website/pages/components-fields.mdx b/website/pages/components-fields.mdx new file mode 100644 index 0000000..2d2d817 --- /dev/null +++ b/website/pages/components-fields.mdx @@ -0,0 +1,39 @@ +--- +title: Fields +description: LiquidTextField and LiquidSearchField — text input backed by native UITextField and UISearchTextField on iOS. +--- + +# Fields + +`LiquidTextField` is a Liquid Glass text input, and `LiquidSearchField` is the search-styled +variant. + +```kotlin +import io.github.androidpoet.liquidkit.field.LiquidTextField +import io.github.androidpoet.liquidkit.field.LiquidSearchField + +@Composable +fun Fields() { + var name by remember { mutableStateOf("") } + var query by remember { mutableStateOf("") } + + LiquidTextField( + value = name, + onValueChange = { name = it }, + placeholder = "Full name", + ) + + LiquidSearchField( + value = query, + onValueChange = { query = it }, + placeholder = "Search", + onSearch = { /* … */ }, + ) +} +``` + +## Android vs iOS + +- **Android** — the field is drawn on a Liquid Glass surface by the shader engine. +- **iOS** — wraps a native `UITextField` (and `UISearchTextField` for search), so editing, + keyboard handling and the system look are all native. diff --git a/website/pages/components-menus-pickers.mdx b/website/pages/components-menus-pickers.mdx new file mode 100644 index 0000000..bbeadef --- /dev/null +++ b/website/pages/components-menus-pickers.mdx @@ -0,0 +1,39 @@ +--- +title: Menus & Pickers +description: LiquidDropdownMenu and LiquidPicker — selection controls backed by native UIMenu and UIPickerView on iOS. +--- + +# Menus & Pickers + +`LiquidDropdownMenu` shows a list of `LiquidMenuItem`s from an anchor, and `LiquidPicker` presents +a set of `LiquidPickerOption`s for single selection. + +```kotlin +import io.github.androidpoet.liquidkit.dropdown.LiquidDropdownMenu +import io.github.androidpoet.liquidkit.dropdown.LiquidMenuItem +import io.github.androidpoet.liquidkit.picker.LiquidPicker +import io.github.androidpoet.liquidkit.picker.LiquidPickerOption + +@Composable +fun MenusAndPickers() { + val items = SortOrder.entries.map { LiquidMenuItem(key = it, label = it.label) } + LiquidDropdownMenu( + items = items, + selectedKey = sort, + onSelected = { sort = it }, + ) + + val options = TimeZone.entries.map { LiquidPickerOption(key = it, label = it.label) } + LiquidPicker( + options = options, + selectedKey = zone, + onSelected = { zone = it }, + ) +} +``` + +## Android vs iOS + +- **Android** — rendered as Liquid Glass surfaces by the shader engine. +- **iOS** — uses genuine native `UIButton` + `UIMenu` for the dropdown and `UIPickerView` for the + picker, so they behave exactly like system controls. diff --git a/website/pages/components-stepper-media.mdx b/website/pages/components-stepper-media.mdx new file mode 100644 index 0000000..398dbb3 --- /dev/null +++ b/website/pages/components-stepper-media.mdx @@ -0,0 +1,38 @@ +--- +title: Stepper & Media +description: LiquidStepper and LiquidMediaControl — value steppers and transport controls with native UIStepper on iOS. +--- + +# Stepper & Media + +`LiquidStepper` increments and decrements a numeric value, and `LiquidMediaControl` provides glass +transport controls for playback. + +```kotlin +import io.github.androidpoet.liquidkit.stepper.LiquidStepper +import io.github.androidpoet.liquidkit.play.LiquidMediaControl + +@Composable +fun StepperAndMedia() { + var quantity by remember { mutableFloatStateOf(2f) } + LiquidStepper( + value = quantity, + onValueChange = { quantity = it }, + range = 0f..10f, + ) + + var playing by remember { mutableStateOf(false) } + LiquidMediaControl( + isPlaying = playing, + onPlayPause = { playing = !playing }, + onPrevious = { /* … */ }, + onNext = { /* … */ }, + ) +} +``` + +## Android vs iOS + +- **Android** — both render as Liquid Glass surfaces via the shader engine. +- **iOS** — the stepper wraps a native `UIStepper`; the media control renders native transport + controls, picking up system Liquid Glass on iOS 26. diff --git a/website/pages/components-surfaces.mdx b/website/pages/components-surfaces.mdx new file mode 100644 index 0000000..9681974 --- /dev/null +++ b/website/pages/components-surfaces.mdx @@ -0,0 +1,29 @@ +--- +title: Surfaces +description: LiquidSurface and LiquidGlassContainer — the building-block glass surfaces of LiquidKit. +--- + +# Surfaces + +`LiquidSurface` is a single glass surface you can place behind any content; `LiquidGlassContainer` +groups multiple surfaces so they share a consistent backdrop. + +```kotlin +import io.github.androidpoet.liquidkit.surface.LiquidSurface +import io.github.androidpoet.liquidkit.surface.LiquidGlassContainer + +@Composable +fun Surfaces() { + LiquidGlassContainer { + LiquidSurface(interactive = true) { + Text("Frosted content on glass") + } + } +} +``` + +## Android vs iOS + +- **Android** — the surface is rendered by the internal backdrop / refraction shader engine. +- **iOS** — backed by a `UIVisualEffectView`. On iOS 26 it uses the typed `UIGlassEffect` + (with tint and interactivity); earlier versions fall back to a `UIBlurEffect` `systemMaterial`. diff --git a/website/pages/components-toggles-sliders.mdx b/website/pages/components-toggles-sliders.mdx new file mode 100644 index 0000000..55a19df --- /dev/null +++ b/website/pages/components-toggles-sliders.mdx @@ -0,0 +1,41 @@ +--- +title: Toggles, Sliders & Segmented +description: LiquidToggle, LiquidSlider, LiquidSegmentedControl and navigation — the core controls of LiquidKit. +--- + +# Toggles, Sliders & Segmented + +The core control set: `LiquidToggle`, `LiquidSlider`, `LiquidSegmentedControl` (with +`LiquidSegment`), plus navigation via `LiquidTabScaffold`. + +```kotlin +import io.github.androidpoet.liquidkit.toggle.LiquidToggle +import io.github.androidpoet.liquidkit.slider.LiquidSlider +import io.github.androidpoet.liquidkit.segmented.LiquidSegment +import io.github.androidpoet.liquidkit.segmented.LiquidSegmentedControl + +@Composable +fun Controls() { + var on by remember { mutableStateOf(true) } + LiquidToggle(checked = on, onCheckedChange = { on = it }) + + var value by remember { mutableStateOf(0.5f) } + LiquidSlider(value = value, onValueChange = { value = it }) + + val segments = Density.entries.map { LiquidSegment(key = it, label = it.title) } + LiquidSegmentedControl( + segments = segments, + selectedKey = density, + onSelected = { density = it }, + ) +} +``` + +For tabbed navigation, use `LiquidTabScaffold` (or the optional +`io.github.androidpoet:liquidkit-navigation3` helpers for a Navigation 3 tab stack). + +## Android vs iOS + +- **Android** — every control is rendered by the internal Liquid Glass shader engine. +- **iOS** — each wraps the matching native control (`UISwitch`, `UISlider`, + `UISegmentedControl`, and a native tab bar), so they adopt system Liquid Glass on iOS 26. diff --git a/website/pages/components-toolbar-sheet.mdx b/website/pages/components-toolbar-sheet.mdx new file mode 100644 index 0000000..3c8d093 --- /dev/null +++ b/website/pages/components-toolbar-sheet.mdx @@ -0,0 +1,39 @@ +--- +title: Toolbar & Sheet +description: LiquidToolbar and LiquidSheet — top app bar and bottom sheet with native UINavigationBar and glass surfaces on iOS. +--- + +# Toolbar & Sheet + +`LiquidToolbar` is a Liquid Glass top app bar with leading and trailing action slots, and +`LiquidSheet` is a glass bottom sheet. + +```kotlin +import io.github.androidpoet.liquidkit.toolbar.LiquidToolbar +import io.github.androidpoet.liquidkit.sheet.LiquidSheet + +@Composable +fun Chrome() { + LiquidToolbar( + title = "Library", + leading = { LiquidIcon(/* back */) }, + trailing = { LiquidIcon(/* more */) }, + ) + + var sheetVisible by remember { mutableStateOf(false) } + LiquidSheet( + visible = sheetVisible, + onDismiss = { sheetVisible = false }, + ) { + Text("Glass sheet content") + } +} +``` + +## Android vs iOS + +- **Android** — both render as Liquid Glass surfaces via the shader engine. +- **iOS** — the toolbar wraps a native `UINavigationBar`; the sheet renders a native glass surface + (`UIVisualEffectView` with `UIGlassEffect` on iOS 26, `UIBlurEffect` `systemMaterial` fallback on + earlier versions). Native sheet presentation — detents, grabber, interactive dismiss — stays with + the SwiftUI host. diff --git a/website/pages/getting-started.mdx b/website/pages/getting-started.mdx new file mode 100644 index 0000000..9897b6d --- /dev/null +++ b/website/pages/getting-started.mdx @@ -0,0 +1,74 @@ +--- +title: Getting Started +description: Add LiquidKit to your Compose Multiplatform project from Maven Central and consume it on Android and iOS. +--- + +import { Steps, Callout } from 'nextra/components' + +# Getting Started + +LiquidKit is published to **Maven Central** under the coordinates +`io.github.androidpoet:liquidkit`, with optional Navigation 3 tab helpers in +`io.github.androidpoet:liquidkit-navigation3`. + + + +### Add the dependency + +In your Compose Multiplatform module, add LiquidKit to `commonMain`: + +```kotlin +commonMain.dependencies { + implementation("io.github.androidpoet:liquidkit:") + // optional Navigation 3 tab helpers: + implementation("io.github.androidpoet:liquidkit-navigation3:") +} +``` + +### Or use a version catalog + +In `gradle/libs.versions.toml`: + +```toml +[versions] +liquidkit = "" + +[libraries] +liquidkit = { module = "io.github.androidpoet:liquidkit", version.ref = "liquidkit" } +liquidkit-navigation3 = { module = "io.github.androidpoet:liquidkit-navigation3", version.ref = "liquidkit" } +``` + +```kotlin +commonMain.dependencies { + implementation(libs.liquidkit) + implementation(libs.liquidkit.navigation3) +} +``` + +### Use a component + +```kotlin +import io.github.androidpoet.liquidkit.button.LiquidButton + +@Composable +fun Screen() { + LiquidButton(onClick = { /* … */ }) { + Text("Get started") + } +} +``` + +### Consume the iOS framework + +On iOS, the KMP build produces a static framework named **`LiquidKit`** (and +**`LiquidKitNavigation3`** for the navigation helpers). Consume it the same way you embed +any Kotlin/Native framework — the bundled `iosApp` links the framework produced by the +multiplatform build, and the components render through their native UIKit counterparts. + + + + +On **iOS 26** LiquidKit components render with the authentic `UIGlassEffect` Liquid Glass +material. On earlier iOS versions they fall back to a `UIBlurEffect` `systemMaterial`, so the +same code keeps working and simply renders the closest available material. + diff --git a/website/pages/index.mdx b/website/pages/index.mdx new file mode 100644 index 0000000..3c240e9 --- /dev/null +++ b/website/pages/index.mdx @@ -0,0 +1,57 @@ +--- +title: LiquidKit +description: Liquid Glass UI for Compose Multiplatform — Android shader rendering, genuine native iOS controls. +--- + +import { Hero } from '../components/Hero' +import { Cards } from 'nextra/components' + + + +LiquidKit is a Kotlin Multiplatform component kit that brings the Liquid Glass look to +Compose Multiplatform. You write against **one shared Compose API**; each platform renders it +the right way — Android draws the glass with an internal backdrop / Liquid Glass shader engine, +while iOS wraps **genuine native UIKit controls** for authentic system Liquid Glass on iOS 26. + +```kotlin +import io.github.androidpoet.liquidkit.button.LiquidButton +import io.github.androidpoet.liquidkit.toggle.LiquidToggle + +@Composable +fun Demo() { + var on by remember { mutableStateOf(true) } + + LiquidButton(onClick = { /* … */ }) { + Text("Continue") + } + + LiquidToggle( + checked = on, + onCheckedChange = { on = it }, + ) +} +``` + +## Why LiquidKit + +- **One cross-platform Compose API** — the same composables on Android and iOS. +- **Android shader rendering** — an internal backdrop capture + refraction / Liquid Glass shader + engine renders the glass look in-Compose, no platform views required. +- **Genuine native iOS controls** — each component wraps real UIKit controls, so on iOS 26 they + pick up authentic system Liquid Glass, with a graceful blur fallback on earlier versions. +- **Full iOS-parity component set** — buttons, surfaces, menus, pickers, fields, steppers, media + controls, toolbars, sheets, toggles, sliders, segmented controls and navigation. + +## Explore + + + + + + + + + + + + diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml new file mode 100644 index 0000000..0d79464 --- /dev/null +++ b/website/pnpm-lock.yaml @@ -0,0 +1,4142 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + postcss: ^8.5.10 + +importers: + + .: + dependencies: + next: + specifier: ^15.5.18 + version: 15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nextra: + specifier: ^3.3.1 + version: 3.3.1(@types/react@19.2.17)(next@15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + nextra-theme-docs: + specifier: ^3.3.1 + version: 3.3.1(next@15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(@types/react@19.2.17)(next@15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + +packages: + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@formatjs/intl-localematcher@0.5.10': + resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==} + + '@headlessui/react@2.2.10': + resolution: {integrity: sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.3': + resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} + + '@internationalized/number@3.6.7': + resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} + + '@internationalized/string@3.2.9': + resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==} + + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@mermaid-js/parser@1.1.1': + resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} + + '@napi-rs/simple-git-android-arm-eabi@0.1.22': + resolution: {integrity: sha512-JQZdnDNm8o43A5GOzwN/0Tz3CDBQtBUNqzVwEopm32uayjdjxev1Csp1JeaqF3v9djLDIvsSE39ecsN2LhCKKQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/simple-git-android-arm64@0.1.22': + resolution: {integrity: sha512-46OZ0SkhnvM+fapWjzg/eqbJvClxynUpWYyYBn4jAj7GQs1/Yyc8431spzDmkA8mL0M7Xo8SmbkzTDE7WwYAfg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/simple-git-darwin-arm64@0.1.22': + resolution: {integrity: sha512-zH3h0C8Mkn9//MajPI6kHnttywjsBmZ37fhLX/Fiw5XKu84eHA6dRyVtMzoZxj6s+bjNTgaMgMUucxPn9ktxTQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/simple-git-darwin-x64@0.1.22': + resolution: {integrity: sha512-GZN7lRAkGKB6PJxWsoyeYJhh85oOOjVNyl+/uipNX8bR+mFDCqRsCE3rRCFGV9WrZUHXkcuRL2laIRn7lLi3ag==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/simple-git-freebsd-x64@0.1.22': + resolution: {integrity: sha512-xyqX1C5I0WBrUgZONxHjZH5a4LqQ9oki3SKFAVpercVYAcx3pq6BkZy1YUOP4qx78WxU1CCNfHBN7V+XO7D99A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/simple-git-linux-arm-gnueabihf@0.1.22': + resolution: {integrity: sha512-4LOtbp9ll93B9fxRvXiUJd1/RM3uafMJE7dGBZGKWBMGM76+BAcCEUv2BY85EfsU/IgopXI6n09TycRfPWOjxA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/simple-git-linux-arm64-gnu@0.1.22': + resolution: {integrity: sha512-GVOjP/JjCzbQ0kSqao7ctC/1sodVtv5VF57rW9BFpo2y6tEYPCqHnkQkTpieuwMNe+TVOhBUC1+wH0d9/knIHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/simple-git-linux-arm64-musl@0.1.22': + resolution: {integrity: sha512-MOs7fPyJiU/wqOpKzAOmOpxJ/TZfP4JwmvPad/cXTOWYwwyppMlXFRms3i98EU3HOazI/wMU2Ksfda3+TBluWA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/simple-git-linux-ppc64-gnu@0.1.22': + resolution: {integrity: sha512-L59dR30VBShRUIZ5/cQHU25upNgKS0AMQ7537J6LCIUEFwwXrKORZKJ8ceR+s3Sr/4jempWVvMdjEpFDE4HYww==} + engines: {node: '>= 10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@napi-rs/simple-git-linux-s390x-gnu@0.1.22': + resolution: {integrity: sha512-4FHkPlCSIZUGC6HiADffbe6NVoTBMd65pIwcd40IDbtFKOgFMBA+pWRqKiQ21FERGH16Zed7XHJJoY3jpOqtmQ==} + engines: {node: '>= 10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@napi-rs/simple-git-linux-x64-gnu@0.1.22': + resolution: {integrity: sha512-Ei1tM5Ho/dwknF3pOzqkNW9Iv8oFzRxE8uOhrITcdlpxRxVrBVptUF6/0WPdvd7R9747D/q61QG/AVyWsWLFKw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/simple-git-linux-x64-musl@0.1.22': + resolution: {integrity: sha512-zRYxg7it0p3rLyEJYoCoL2PQJNgArVLyNavHW03TFUAYkYi5bxQ/UFNVpgxMaXohr5yu7qCBqeo9j4DWeysalg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/simple-git-win32-arm64-msvc@0.1.22': + resolution: {integrity: sha512-XGFR1fj+Y9cWACcovV2Ey/R2xQOZKs8t+7KHPerYdJ4PtjVzGznI4c2EBHXtdOIYvkw7tL5rZ7FN1HJKdD5Quw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/simple-git-win32-ia32-msvc@0.1.22': + resolution: {integrity: sha512-Gqr9Y0gs6hcNBA1IXBpoqTFnnIoHuZGhrYqaZzEvGMLrTrpbXrXVEtX3DAAD2RLc1b87CPcJ49a7sre3PU3Rfw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/simple-git-win32-x64-msvc@0.1.22': + resolution: {integrity: sha512-hQjcreHmUcpw4UrtkOron1/TQObfe484lxiXFLLUj7aWnnnOVs1mnXq5/Bo9+3NYZldFpFRJPdPBeHCisXkKJg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/simple-git@0.1.22': + resolution: {integrity: sha512-bMVoAKhpjTOPHkW/lprDPwv5aD4R4C3Irt8vn+SKA9wudLe9COLxOhurrKRsxmZccUbWXRF7vukNeGUAj5P8kA==} + engines: {node: '>= 10'} + + '@next/env@15.5.19': + resolution: {integrity: sha512-sWWluFvcv5v3Fxznmf2ZfjyoVQt/64oCnYqS90inQWGzMPK1VjvekPiz3OPHKmFT30EnHrjlbyaHLt3M0vWabw==} + + '@next/swc-darwin-arm64@15.5.19': + resolution: {integrity: sha512-jx9wWlTKueHKPvVOndyr7WuaevWCkuYqsQ8gC0TMPKAVWG3MhcdMrjfo9tvIZNXd0QOUYXXvAcZ325y8Uq7uzg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.19': + resolution: {integrity: sha512-291KFcsIQ3OenRdiUDFOR6W3wezzH4auENXm1gbm1Bjd4ANMMRgxPrWTUztQN43BnVoVuMnHCrLeECIMwgFKbA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.19': + resolution: {integrity: sha512-WeH+nelQyyMeE2f8FxBRZNrGipya5zHZV2vjzfCOAYyiI6am+NbnWAAldOBFQBB2w0DjJcsvrKqoFT2b7+5YoA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@15.5.19': + resolution: {integrity: sha512-5xTOE0lDlDCSSfp+BAif7j17VRRCjWp//ZPZy6NI0QpdrhxtQnsZguSx0xAAZ0c9XZLrLLwCe/XVe5YPrRilKw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@15.5.19': + resolution: {integrity: sha512-LTxRmMgqqMv05Had879W00Fm53quiJd3Zuz8h1JSNJ3nGSlbZ/7Tjs1tKyScgN3Au3t3MyPsjPlq60fMmSHLsg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@15.5.19': + resolution: {integrity: sha512-eoNQSpA5PQfB9wBO4RA47MTDXWz1fizy9Y3Z6e4DetYIF3dvjuu8sj7aIGn/bFCU6lnFzTK34NtCaffP4NsQ7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@15.5.19': + resolution: {integrity: sha512-6UNt2dFuCHOe446sm/Kp69nUe8/wIhnh9bm6Xcqw4qEWCOppLMOvhTBVgvM7invVUNr4SPpP6NOQsACtn2IN9Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.19': + resolution: {integrity: sha512-PhmojAHyqMne56HBLGu9dhDnHPuFmEjrXSQMM/nW0J6j849lk3ESrVtqNJcCk8CKOV7brpTTbaYAjwKPzKM69w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@react-aria/focus@3.22.1': + resolution: {integrity: sha512-CPxtkyrBi/HYY5P3lE/57sQ6qfa0lN8E55TOm89H0kNGv0lKt+/0zP7lWERzBjRr5IxBVrQX4gFEowBN52LPaA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.28.1': + resolution: {integrity: sha512-Bqb+HrD5I5MHS2SKBhISYqo2SW8Y2dfzgF/Y1lIJq7xqLxheo9vzxPGEHhz+XzkgGfoqEJx8A6a3C7uiqS3HWA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.36.0': + resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@shikijs/core@1.29.2': + resolution: {integrity: sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==} + + '@shikijs/engine-javascript@1.29.2': + resolution: {integrity: sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==} + + '@shikijs/engine-oniguruma@1.29.2': + resolution: {integrity: sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==} + + '@shikijs/langs@1.29.2': + resolution: {integrity: sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==} + + '@shikijs/themes@1.29.2': + resolution: {integrity: sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==} + + '@shikijs/twoslash@1.29.2': + resolution: {integrity: sha512-2S04ppAEa477tiaLfGEn1QJWbZUmbk8UoPbAEw4PifsrxkBXtAtOflIZJNtuCwz8ptc/TPxy7CO7gW4Uoi6o/g==} + + '@shikijs/types@1.29.2': + resolution: {integrity: sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@tanstack/react-virtual@3.14.3': + resolution: {integrity: sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.17.1': + resolution: {integrity: sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==} + + '@theguild/remark-mermaid@0.1.3': + resolution: {integrity: sha512-2FjVlaaKXK7Zj7UJAgOVTyaahn/3/EAfqYhyXg0BfDBVUl+lXcoIWRaxzqfnDr2rv8ax6GsC5mNh6hAaT86PDw==} + peerDependencies: + react: ^18.2.0 + + '@theguild/remark-npm2yarn@0.3.3': + resolution: {integrity: sha512-ma6DvR03gdbvwqfKx1omqhg9May/VYGdMHvTzB4VuxkyS7KzfZ/lzrj43hmcsggpMje0x7SADA/pcMph0ejRnA==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.14': + resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript/vfs@1.6.4': + resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==} + peerDependencies: + typescript: '*' + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + better-react-mathjax@2.3.0: + resolution: {integrity: sha512-K0ceQC+jQmB+NLDogO5HCpqmYf18AU2FxDbLdduYgkHYWZApFggkHE4dIaXCV1NqeoscESYXXo1GSkY6fA295w==} + peerDependencies: + react: '>=16.8' + + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clipboardy@4.0.0: + resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} + engines: {node: '>=18'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + compute-scroll-into-view@3.1.1: + resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.34.0: + resolution: {integrity: sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-toolkit@1.47.1: + resolution: {integrity: sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-value-to-estree@3.5.0: + resolution: {integrity: sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + flexsearch@0.7.43: + resolution: {integrity: sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + hast-util-from-dom@5.0.1: + resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + is64bit@2.0.0: + resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + mathjax-full@3.2.2: + resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + deprecated: Version 4 replaces this package with the scoped package @mathjax/src + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + mermaid@11.15.0: + resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} + + mhchemparser@4.2.1: + resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mj-context-menu@0.6.1: + resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.13: + resolution: {integrity: sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + next@15.5.19: + resolution: {integrity: sha512-xNOW6tYshGX1/Oi3F8uuk4gpDeWsSUE/1Z0G5uUMekIxaQ0xc03UXd9II0VQHYMWviMeA0OHpJFAKsHf8bTYVg==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + nextra-theme-docs@3.3.1: + resolution: {integrity: sha512-P305m2UcW2IDyQhjrcAu0qpdPArikofinABslUCAyixYShsmcdDRUhIMd4QBHYru4gQuVjGWX9PhWZZCbNvzDQ==} + peerDependencies: + next: '>=13' + nextra: 3.3.1 + react: '>=18' + react-dom: '>=18' + + nextra@3.3.1: + resolution: {integrity: sha512-jiwj+LfUPHHeAxJAEqFuglxnbjFgzAOnDWFsjv7iv3BWiX8OksDwd3I2Sv3j2zba00iIBDEPdNeylfzTtTLZVg==} + engines: {node: '>=18'} + peerDependencies: + next: '>=13' + react: '>=18' + react-dom: '>=18' + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npm-to-yarn@3.0.1: + resolution: {integrity: sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + oniguruma-to-es@2.3.0: + resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==} + + p-limit@6.2.0: + resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} + engines: {node: '>=18'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse-numeric-range@1.3.0: + resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + react-aria@3.50.0: + resolution: {integrity: sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-medium-image-zoom@5.4.8: + resolution: {integrity: sha512-72CIldEUaPejjcaDOYIeDsGlWzNKpmxyKgiPi1LBCkWCIGHDLlA2KTeo2uNtmN/m72Y3k/2uWDfon9SDiPbHaA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-stately@3.48.0: + resolution: {integrity: sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + reading-time@1.5.0: + resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + regex-recursion@5.1.1: + resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@5.1.1: + resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==} + + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-pretty-code@0.14.0: + resolution: {integrity: sha512-hBeKF/Wkkf3zyUS8lal9RCUuhypDWLQc+h9UrP9Pav25FUm/AQAVh4m5gdvJxh4Oz+U+xKvdsV01p1LdvsZTiQ==} + engines: {node: '>=18'} + peerDependencies: + shiki: ^1.3.0 + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-reading-time@2.1.0: + resolution: {integrity: sha512-gBsJbQv87TUq4dRMSOgIX6P60Tk9ke8c29KsL7bccmsv2m9AycDfVu3ghRtrNpHLZU3TE5P/vImGOMSPzYU8rA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shiki@1.29.2: + resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speech-rule-engine@4.1.4: + resolution: {integrity: sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==} + hasBin: true + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + + system-architecture@0.1.0: + resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} + engines: {node: '>=18'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + title@4.0.1: + resolution: {integrity: sha512-xRnPkJx9nvE5MF6LkB5e8QJjE2FW8269wTu/LQdf7zZqBgPly0QJPf/CWAo7srj5so4yXfoLEdCFgurlpi47zg==} + hasBin: true + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + twoslash-protocol@0.2.12: + resolution: {integrity: sha512-5qZLXVYfZ9ABdjqbvPc4RWMr7PrpPaaDSeaYY55vl/w1j6H6kzsWK/urAEIXlzYlyrFmyz1UbwIt+AA0ck+wbg==} + + twoslash@0.2.12: + resolution: {integrity: sha512-tEHPASMqi7kqwfJbkk7hc/4EhlrKCSLcur+TcvYki3vhIfaRMXnXjaYFgXpoZRbT6GdprD4tGuVBEmTpUgLBsw==} + peerDependencies: + typescript: '*' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-remove@4.0.0: + resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wicked-good-xpath@1.3.0: + resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + zod-validation-error@3.5.4: + resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.24.4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.2.4 + + '@braintree/sanitize-url@7.1.2': {} + + '@chevrotain/types@11.1.2': {} + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.11 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.4.0 + + '@floating-ui/utils@0.2.11': {} + + '@formatjs/intl-localematcher@0.5.10': + dependencies: + tslib: 2.8.1 + + '@headlessui/react@2.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': 3.14.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.3': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@internationalized/date@3.12.2': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/number@3.6.7': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/string@3.2.9': + dependencies: + '@swc/helpers': 0.5.23 + + '@mdx-js/mdx@3.1.1': + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.14 + acorn: 8.17.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.17.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@18.3.1)': + dependencies: + '@types/mdx': 2.0.14 + '@types/react': 19.2.17 + react: 18.3.1 + + '@mermaid-js/parser@1.1.1': + dependencies: + '@chevrotain/types': 11.1.2 + + '@napi-rs/simple-git-android-arm-eabi@0.1.22': + optional: true + + '@napi-rs/simple-git-android-arm64@0.1.22': + optional: true + + '@napi-rs/simple-git-darwin-arm64@0.1.22': + optional: true + + '@napi-rs/simple-git-darwin-x64@0.1.22': + optional: true + + '@napi-rs/simple-git-freebsd-x64@0.1.22': + optional: true + + '@napi-rs/simple-git-linux-arm-gnueabihf@0.1.22': + optional: true + + '@napi-rs/simple-git-linux-arm64-gnu@0.1.22': + optional: true + + '@napi-rs/simple-git-linux-arm64-musl@0.1.22': + optional: true + + '@napi-rs/simple-git-linux-ppc64-gnu@0.1.22': + optional: true + + '@napi-rs/simple-git-linux-s390x-gnu@0.1.22': + optional: true + + '@napi-rs/simple-git-linux-x64-gnu@0.1.22': + optional: true + + '@napi-rs/simple-git-linux-x64-musl@0.1.22': + optional: true + + '@napi-rs/simple-git-win32-arm64-msvc@0.1.22': + optional: true + + '@napi-rs/simple-git-win32-ia32-msvc@0.1.22': + optional: true + + '@napi-rs/simple-git-win32-x64-msvc@0.1.22': + optional: true + + '@napi-rs/simple-git@0.1.22': + optionalDependencies: + '@napi-rs/simple-git-android-arm-eabi': 0.1.22 + '@napi-rs/simple-git-android-arm64': 0.1.22 + '@napi-rs/simple-git-darwin-arm64': 0.1.22 + '@napi-rs/simple-git-darwin-x64': 0.1.22 + '@napi-rs/simple-git-freebsd-x64': 0.1.22 + '@napi-rs/simple-git-linux-arm-gnueabihf': 0.1.22 + '@napi-rs/simple-git-linux-arm64-gnu': 0.1.22 + '@napi-rs/simple-git-linux-arm64-musl': 0.1.22 + '@napi-rs/simple-git-linux-ppc64-gnu': 0.1.22 + '@napi-rs/simple-git-linux-s390x-gnu': 0.1.22 + '@napi-rs/simple-git-linux-x64-gnu': 0.1.22 + '@napi-rs/simple-git-linux-x64-musl': 0.1.22 + '@napi-rs/simple-git-win32-arm64-msvc': 0.1.22 + '@napi-rs/simple-git-win32-ia32-msvc': 0.1.22 + '@napi-rs/simple-git-win32-x64-msvc': 0.1.22 + + '@next/env@15.5.19': {} + + '@next/swc-darwin-arm64@15.5.19': + optional: true + + '@next/swc-darwin-x64@15.5.19': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.19': + optional: true + + '@next/swc-linux-arm64-musl@15.5.19': + optional: true + + '@next/swc-linux-x64-gnu@15.5.19': + optional: true + + '@next/swc-linux-x64-musl@15.5.19': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.19': + optional: true + + '@next/swc-win32-x64-msvc@15.5.19': + optional: true + + '@react-aria/focus@3.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/interactions@3.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + react-aria: 3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + + '@react-types/shared@3.36.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@shikijs/core@1.29.2': + dependencies: + '@shikijs/engine-javascript': 1.29.2 + '@shikijs/engine-oniguruma': 1.29.2 + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 2.3.0 + + '@shikijs/engine-oniguruma@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + + '@shikijs/themes@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + + '@shikijs/twoslash@1.29.2(typescript@6.0.3)': + dependencies: + '@shikijs/core': 1.29.2 + '@shikijs/types': 1.29.2 + twoslash: 0.2.12(typescript@6.0.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@shikijs/types@1.29.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@tanstack/react-virtual@3.14.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.17.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/virtual-core@3.17.1': {} + + '@theguild/remark-mermaid@0.1.3(react@18.3.1)': + dependencies: + mermaid: 11.15.0 + react: 18.3.1 + unist-util-visit: 5.1.0 + + '@theguild/remark-npm2yarn@0.3.3': + dependencies: + npm-to-yarn: 3.0.1 + unist-util-visit: 5.1.0 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + + '@types/estree@1.0.9': {} + + '@types/geojson@7946.0.16': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/katex@0.16.8': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.14': {} + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@typescript/vfs@1.6.4(typescript@6.0.3)': + dependencies: + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@ungap/structured-clone@1.3.1': {} + + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@xmldom/xmldom@0.9.10': {} + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + array-iterate@2.0.1: {} + + astring@1.9.0: {} + + bail@2.0.2: {} + + better-react-mathjax@2.3.0(react@18.3.1): + dependencies: + mathjax-full: 3.2.2 + react: 18.3.1 + + caniuse-lite@1.0.30001799: {} + + ccount@2.0.1: {} + + chalk@5.6.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + client-only@0.0.1: {} + + clipboardy@4.0.0: + dependencies: + execa: 8.0.1 + is-wsl: 3.1.1 + is64bit: 2.0.0 + + clsx@2.1.1: {} + + collapse-white-space@2.1.0: {} + + comma-separated-tokens@2.0.3: {} + + commander@13.1.0: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + compute-scroll-into-view@3.1.1: {} + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.34.0 + + cytoscape-fcose@2.2.0(cytoscape@3.34.0): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.34.0 + + cytoscape@3.34.0: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + + dayjs@1.11.21: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + dequal@2.0.3: {} + + detect-libc@2.1.2: + optional: true + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dompurify@3.4.11: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + emoji-regex-xs@1.0.0: {} + + entities@6.0.1: {} + + es-toolkit@1.47.1: {} + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.17.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + + escape-string-regexp@5.0.0: {} + + esm@3.2.25: {} + + esprima@4.0.1: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.9 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + + estree-util-value-to-estree@3.5.0: + dependencies: + '@types/estree': 1.0.9 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + fault@2.0.1: + dependencies: + format: 0.2.2 + + flexsearch@0.7.43: {} + + format@0.2.2: {} + + get-stream@8.0.1: {} + + github-slugger@2.0.0: {} + + graceful-fs@4.2.11: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + hachure-fill@0.5.2: {} + + hast-util-from-dom@5.0.1: + dependencies: + '@types/hast': 3.0.4 + hastscript: 9.0.1 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.1 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.2.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.1 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + + html-void-elements@3.0.0: {} + + human-signals@5.0.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + import-meta-resolve@4.2.0: {} + + inline-style-parser@0.2.7: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + + is-hexadecimal@2.0.1: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-plain-obj@4.1.0: {} + + is-stream@3.0.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + is64bit@2.0.0: + dependencies: + system-architecture: 0.1.0 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + katex@0.16.47: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + kind-of@6.0.3: {} + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + lodash-es@4.18.1: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + markdown-extensions@2.0.0: {} + + markdown-table@3.0.4: {} + + marked@16.4.2: {} + + mathjax-full@3.2.2: + dependencies: + esm: 3.2.25 + mhchemparser: 4.2.1 + mj-context-menu: 0.6.1 + speech-rule-engine: 4.1.4 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + merge-stream@2.0.0: {} + + mermaid@11.15.0: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.3 + '@mermaid-js/parser': 1.1.1 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.34.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.0) + cytoscape-fcose: 2.2.0(cytoscape@3.34.0) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.21 + dompurify: 3.4.11 + es-toolkit: 1.47.1 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.3.0 + uuid: 14.0.0 + + mhchemparser@4.2.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.47 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.9 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + mimic-fn@4.0.0: {} + + mj-context-menu@0.6.1: {} + + ms@2.1.3: {} + + nanoid@3.3.13: {} + + negotiator@1.0.0: {} + + next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + next@15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 15.5.19 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001799 + postcss: 8.5.15 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.19 + '@next/swc-darwin-x64': 15.5.19 + '@next/swc-linux-arm64-gnu': 15.5.19 + '@next/swc-linux-arm64-musl': 15.5.19 + '@next/swc-linux-x64-gnu': 15.5.19 + '@next/swc-linux-x64-musl': 15.5.19 + '@next/swc-win32-arm64-msvc': 15.5.19 + '@next/swc-win32-x64-msvc': 15.5.19 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + nextra-theme-docs@3.3.1(next@15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(@types/react@19.2.17)(next@15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@headlessui/react': 2.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: 2.1.1 + escape-string-regexp: 5.0.0 + flexsearch: 0.7.43 + next: 15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nextra: 3.3.1(@types/react@19.2.17)(next@15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + scroll-into-view-if-needed: 3.1.0 + zod: 3.25.76 + + nextra@3.3.1(@types/react@19.2.17)(next@15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3): + dependencies: + '@formatjs/intl-localematcher': 0.5.10 + '@headlessui/react': 2.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mdx-js/mdx': 3.1.1 + '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@18.3.1) + '@napi-rs/simple-git': 0.1.22 + '@shikijs/twoslash': 1.29.2(typescript@6.0.3) + '@theguild/remark-mermaid': 0.1.3(react@18.3.1) + '@theguild/remark-npm2yarn': 0.3.3 + better-react-mathjax: 2.3.0(react@18.3.1) + clsx: 2.1.1 + estree-util-to-js: 2.0.0 + estree-util-value-to-estree: 3.5.0 + github-slugger: 2.0.0 + graceful-fs: 4.2.11 + gray-matter: 4.0.3 + hast-util-to-estree: 3.1.3 + katex: 0.16.47 + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm: 3.1.0 + mdast-util-to-hast: 13.2.1 + negotiator: 1.0.0 + next: 15.5.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + p-limit: 6.2.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-medium-image-zoom: 5.4.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rehype-katex: 7.0.1 + rehype-pretty-code: 0.14.0(shiki@1.29.2) + rehype-raw: 7.0.0 + remark-frontmatter: 5.0.0 + remark-gfm: 4.0.1 + remark-math: 6.0.0 + remark-reading-time: 2.1.0 + remark-smartypants: 3.0.2 + shiki: 1.29.2 + slash: 5.1.0 + title: 4.0.1 + unist-util-remove: 4.0.0 + unist-util-visit: 5.1.0 + yaml: 2.9.0 + zod: 3.25.76 + zod-validation-error: 3.5.4(zod@3.25.76) + transitivePeerDependencies: + - '@types/react' + - supports-color + - typescript + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + npm-to-yarn@3.0.1: {} + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + oniguruma-to-es@2.3.0: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 5.1.1 + regex-recursion: 5.1.1 + + p-limit@6.2.0: + dependencies: + yocto-queue: 1.2.2 + + package-manager-detector@1.6.0: {} + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse-numeric-range@1.3.0: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-data-parser@0.1.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + picocolors@1.1.1: {} + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss@8.5.15: + dependencies: + nanoid: 3.3.13 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + property-information@7.2.0: {} + + react-aria@3.50.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + aria-hidden: 1.2.6 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-stately: 3.48.0(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-medium-image-zoom@5.4.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-stately@3.48.0(react@18.3.1): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.36.0(react@18.3.1) + '@swc/helpers': 0.5.23 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + reading-time@1.5.0: {} + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.1(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.9 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + regex-recursion@5.1.1: + dependencies: + regex: 5.1.1 + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@5.1.1: + dependencies: + regex-utilities: 2.3.0 + + rehype-katex@7.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.8 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 + katex: 0.16.47 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-pretty-code@0.14.0(shiki@1.29.2): + dependencies: + '@types/hast': 3.0.4 + hast-util-to-string: 3.0.1 + parse-numeric-range: 1.3.0 + rehype-parse: 9.0.1 + shiki: 1.29.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-reading-time@2.1.0: + dependencies: + estree-util-is-identifier-name: 3.0.0 + estree-util-value-to-estree: 3.5.0 + reading-time: 1.5.0 + unist-util-visit: 5.1.0 + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.1.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + robust-predicates@3.0.3: {} + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + scroll-into-view-if-needed@3.1.0: + dependencies: + compute-scroll-into-view: 3.1.1 + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + semver@7.8.4: + optional: true + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shiki@1.29.2: + dependencies: + '@shikijs/core': 1.29.2 + '@shikijs/engine-javascript': 1.29.2 + '@shikijs/engine-oniguruma': 1.29.2 + '@shikijs/langs': 1.29.2 + '@shikijs/themes': 1.29.2 + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + signal-exit@4.1.0: {} + + slash@5.1.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + space-separated-tokens@2.0.2: {} + + speech-rule-engine@4.1.4: + dependencies: + '@xmldom/xmldom': 0.9.10 + commander: 13.1.0 + wicked-good-xpath: 1.3.0 + + sprintf-js@1.0.3: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-bom-string@1.0.0: {} + + strip-final-newline@3.0.0: {} + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + styled-jsx@5.1.6(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + + stylis@4.4.0: {} + + system-architecture@0.1.0: {} + + tabbable@6.4.0: {} + + tinyexec@1.2.4: {} + + title@4.0.1: + dependencies: + arg: 5.0.2 + chalk: 5.6.2 + clipboardy: 4.0.0 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-dedent@2.3.0: {} + + tslib@2.8.1: {} + + twoslash-protocol@0.2.12: {} + + twoslash@0.2.12(typescript@6.0.3): + dependencies: + '@typescript/vfs': 1.6.4(typescript@6.0.3) + twoslash-protocol: 0.2.12 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + unist-util-remove@4.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + uuid@14.0.0: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + web-namespaces@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wicked-good-xpath@1.3.0: {} + + yaml@2.9.0: {} + + yocto-queue@1.2.2: {} + + zod-validation-error@3.5.4(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} + + zwitch@2.0.4: {} diff --git a/website/public/favicon.svg b/website/public/favicon.svg new file mode 100644 index 0000000..bb18b20 --- /dev/null +++ b/website/public/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/website/public/robots.txt b/website/public/robots.txt new file mode 100644 index 0000000..41b7caa --- /dev/null +++ b/website/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://androidpoet.github.io/LiquidKit/sitemap.xml diff --git a/website/styles/globals.css b/website/styles/globals.css new file mode 100644 index 0000000..1a348fe --- /dev/null +++ b/website/styles/globals.css @@ -0,0 +1,141 @@ +:root { + --lk-accent: #2ec5ce; + --lk-accent-2: #4fa3ff; +} + +@keyframes lk-fade-up { + from { + opacity: 0; + transform: translateY(14px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes lk-gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +@keyframes lk-float { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-6px); + } +} + +.lk-hero { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 4rem 1.5rem 2.5rem; + animation: lk-fade-up 0.6s ease both; +} + +.lk-hero-badge { + display: inline-block; + padding: 0.3rem 0.85rem; + border-radius: 999px; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--lk-accent); + background: color-mix(in srgb, var(--lk-accent) 14%, transparent); + border: 1px solid color-mix(in srgb, var(--lk-accent) 35%, transparent); + margin-bottom: 1.2rem; +} + +.lk-hero-title { + font-size: clamp(2.8rem, 8vw, 4.5rem); + font-weight: 800; + line-height: 1.02; + margin: 0; + background: linear-gradient(110deg, var(--lk-accent-2), var(--lk-accent), var(--lk-accent-2)); + background-size: 220% 220%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + animation: lk-gradient 8s ease infinite; +} + +.lk-hero-subtitle { + max-width: 38rem; + margin: 1.1rem auto 0; + font-size: 1.1rem; + line-height: 1.6; + color: #6b7280; +} + +.dark .lk-hero-subtitle { + color: #9ca3af; +} + +.lk-hero-cta { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + justify-content: center; + margin-top: 2rem; +} + +.lk-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.7rem 1.4rem; + border-radius: 0.7rem; + font-weight: 600; + font-size: 0.95rem; + text-decoration: none; + transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; +} + +.lk-btn:hover { + transform: translateY(-2px); +} + +.lk-btn-primary { + color: #04121a; + background: linear-gradient(120deg, var(--lk-accent-2), var(--lk-accent)); + box-shadow: 0 8px 24px -8px color-mix(in srgb, var(--lk-accent) 70%, transparent); +} + +.lk-btn-primary:hover { + box-shadow: 0 12px 30px -8px color-mix(in srgb, var(--lk-accent) 80%, transparent); +} + +.lk-btn-ghost { + color: inherit; + background: transparent; + border: 1px solid color-mix(in srgb, currentColor 22%, transparent); +} + +.lk-btn-ghost:hover { + border-color: var(--lk-accent); + color: var(--lk-accent); +} + +/* Code-block polish */ +.nextra-code pre { + border-radius: 0.7rem !important; + transition: box-shadow 0.2s ease; +} + +.nextra-code pre:hover { + box-shadow: 0 0 0 1px color-mix(in srgb, var(--lk-accent) 45%, transparent), + 0 10px 30px -12px color-mix(in srgb, var(--lk-accent) 55%, transparent); +} diff --git a/website/theme.config.jsx b/website/theme.config.jsx new file mode 100644 index 0000000..b6707df --- /dev/null +++ b/website/theme.config.jsx @@ -0,0 +1,106 @@ +import { useRouter } from 'next/router' +import { useConfig } from 'nextra-theme-docs' + +const ACCENT = '#2EC5CE' + +const Logo = () => ( + + + LiquidKit + +) + +export default { + logo: , + project: { + link: 'https://github.com/AndroidPoet/LiquidKit', + }, + docsRepositoryBase: 'https://github.com/AndroidPoet/LiquidKit/tree/main/website', + color: { + hue: 200, + saturation: 90, + }, + sidebar: { + defaultMenuCollapseLevel: 1, + }, + toc: { + backToTop: true, + }, + navigation: { + prev: true, + next: true, + }, + darkMode: true, + footer: { + content: ( + + Apache-2.0 © {new Date().getFullYear()}{' '} + + AndroidPoet + + . LiquidKit — Liquid Glass UI for Compose Multiplatform. + + ), + }, + head() { + const { asPath, defaultLocale, locale } = useRouter() + const { frontMatter } = useConfig() + const base = 'https://androidpoet.github.io/LiquidKit' + const url = base + (defaultLocale === locale ? asPath : `/${locale}${asPath}`) + const title = frontMatter.title || 'LiquidKit' + const description = + frontMatter.description || + 'Liquid Glass UI for Compose Multiplatform — Android shader rendering, genuine native iOS controls.' + return ( + <> + + + + + + + + + + + + + ) + }, + useNextSeoProps() { + return { titleTemplate: '%s – LiquidKit' } + }, +} From 3640e63eaad9013b65a6c78d58b0233e3946cac7 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Fri, 19 Jun 2026 09:57:23 +0530 Subject: [PATCH 11/11] Sample: open component showcases on iOS too (parity with Android shell) --- .../liquidkit/sample/LiquidKitSampleApp.kt | 39 +++++++++++++++++++ .../liquidkit/sample/MainViewController.kt | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt index 657cec8..c6c85e4 100644 --- a/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/LiquidKitSampleApp.kt @@ -204,6 +204,45 @@ public fun LiquidKitSampleTabRoot( ) } +/** + * A self-contained tab screen that owns its own component-showcase overlay. + * + * Used by the iOS native [TabView] (one Compose view controller per tab) so iOS + * reaches the exact same full-screen component showcases as the Compose-owned + * Android shell — keeping both platforms the same app while iOS retains its + * native SwiftUI tab chrome. + */ +@Composable +public fun LiquidKitSampleTabScreen( + selectedTab: LiquidKitSampleTab, + modifier: Modifier = Modifier, +) { + var openShowcase by remember { mutableStateOf(null) } + + Box(modifier = modifier.fillMaxSize()) { + LiquidKitSampleTabRoot( + selectedTab = selectedTab, + onOpenShowcase = { openShowcase = it }, + modifier = Modifier.fillMaxSize(), + ) + + val showcase = openShowcase + if (showcase != null) { + Box(modifier = Modifier.fillMaxSize().background(sampleBackground())) { + ShowcaseEntryContent(showcase, modifier = Modifier.fillMaxSize()) + ShowcaseBackChip( + onClick = { openShowcase = null }, + modifier = + Modifier + .align(Alignment.TopStart) + .safeDrawingPadding() + .padding(16.dp), + ) + } + } + } +} + @Composable private fun HomeSamplePanel(onOpenShowcase: (ShowcaseEntry) -> Unit = {}) { var enabled by remember { mutableStateOf(true) } diff --git a/sampleApp/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sample/MainViewController.kt b/sampleApp/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sample/MainViewController.kt index c24df81..576278b 100644 --- a/sampleApp/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sample/MainViewController.kt +++ b/sampleApp/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sample/MainViewController.kt @@ -16,5 +16,5 @@ fun SettingsViewController(): UIViewController = tabRootViewController(LiquidKit private fun tabRootViewController(tab: LiquidKitSampleTab): UIViewController = ComposeUIViewController { - LiquidKitSampleTabRoot(selectedTab = tab) + LiquidKitSampleTabScreen(selectedTab = tab) }