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/.gitignore b/.gitignore index 2dfe1b1..c9bdf66 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ /*/build/ iosApp/DerivedData/ local.properties + +.idea/ +xcuserdata/ diff --git a/README.md b/README.md index 294e526..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 -> @@ -74,10 +122,21 @@ 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. -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( @@ -116,7 +175,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 @@ -138,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 57fe20b..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 @@ -13,8 +17,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/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 b33f002..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 @@ -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 @@ -23,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 @@ -44,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.") @@ -96,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( @@ -112,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( @@ -144,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( @@ -171,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() } @@ -193,12 +204,25 @@ 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, - startRoute = startRoute, - savedStateConfiguration = savedStateConfiguration, - ) + val state = + rememberLiquidNav3TabState( + items = items, + startRoute = startRoute, + savedStateConfiguration = savedStateConfiguration, + ) LiquidNav3TabScaffold( items = items, @@ -207,9 +231,11 @@ public fun LiquidNav3TabScaffold( modifier = modifier, navigationModifier = navigationModifier, style = style, + bottomNavigation = bottomNavigation, ) } +@OptIn(InternalLiquidKitApi::class) @Composable public fun LiquidNav3TabScaffold( items: List>, @@ -218,22 +244,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) + } } } @@ -244,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 new file mode 100644 index 0000000..f08e80e --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.android.kt @@ -0,0 +1,55 @@ +@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.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 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..e8193ee --- /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.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.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/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..516618e --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.android.kt @@ -0,0 +1,230 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +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.fadeIn +import androidx.compose.animation.fadeOut +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 +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 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 +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.InnerShadow +import io.github.androidpoet.liquidkit.internal.androidglass.backdrop.shadow.Shadow + +@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/field/LiquidSearchField.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.android.kt new file mode 100644 index 0000000..a16057b --- /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.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..db963b0 --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.android.kt @@ -0,0 +1,114 @@ +@file:OptIn(io.github.androidpoet.liquidkit.internal.InternalLiquidKitApi::class) + +package io.github.androidpoet.liquidkit.field + +import androidx.compose.foundation.Image +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 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/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/internal/AndroidLiquidGlass.kt index 8b5bd3a..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 @@ -1,54 +1,74 @@ +@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) - } +): 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 = { - blur(style.androidBlurRadius.toPx()) - lens(style.androidRefractionHeight.toPx(), style.androidRefractionHeight.toPx()) - }, - onDrawSurface = { drawRect(style.containerColor) }, - ) - .border(1.dp, Color.White.copy(alpha = 0.34f), shape) -} + 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) + } 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..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 @@ -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,10 +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 com.kyant.backdrop.backdrops.rememberCanvasBackdrop +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( @@ -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( @@ -42,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) }) { @@ -70,24 +82,64 @@ 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( 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, + ), ) } } + +@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/picker/LiquidPicker.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.android.kt new file mode 100644 index 0000000..bd8465d --- /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.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.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/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..dd37090 --- /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.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/segmented/LiquidSegmentedControl.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.android.kt index 7536638..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 @@ -1,17 +1,53 @@ +@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.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 +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.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 com.kyant.backdrop.backdrops.rememberCanvasBackdrop +import com.kyant.shapes.Capsule 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.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.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 @Composable internal actual fun PlatformLiquidSegmentedControl( @@ -22,41 +58,113 @@ 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/sheet/LiquidSheet.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt new file mode 100644 index 0000000..2d89808 --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.android.kt @@ -0,0 +1,90 @@ +@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/slider/LiquidSlider.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/slider/LiquidSlider.android.kt index 9d18407..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 @@ -1,10 +1,15 @@ +@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.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.LiquidSlider @Composable @@ -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/stepper/LiquidStepper.android.kt b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt new file mode 100644 index 0000000..c5a541c --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.android.kt @@ -0,0 +1,93 @@ +@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/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..d32cc23 --- /dev/null +++ b/liquidkit/src/androidMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.android.kt @@ -0,0 +1,133 @@ +@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.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.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/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..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 @@ -1,11 +1,16 @@ +@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.androidglass.catalog.components.LiquidToggle 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 @Composable internal actual fun PlatformLiquidToggle( @@ -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/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..49b0d43 --- /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/LiquidGlassStyle.kt b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/LiquidGlassStyle.kt index 06af915..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,28 +14,62 @@ public data class LiquidGlassStyle( public val selectedContentColor: Color, ) { public companion object { - 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 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), - ) + // ── 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 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), + ) + + // ── 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/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/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/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/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..70c770c --- /dev/null +++ b/liquidkit/src/commonMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.kt @@ -0,0 +1,92 @@ +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/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/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..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 @@ -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,9 +12,15 @@ 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) { +public class LiquidTabState internal constructor( + initialKey: T, +) { public var selectedKey: T by mutableStateOf(initialKey) private set @@ -25,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( @@ -36,6 +44,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 +69,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 +83,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/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/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/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/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/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/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/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..6cd760e --- /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.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.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/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/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/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 new file mode 100644 index 0000000..ea2b3f1 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidButton.ios.kt @@ -0,0 +1,80 @@ +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..2b1508e --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/button/LiquidFab.ios.kt @@ -0,0 +1,88 @@ +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/dropdown/LiquidDropdownMenu.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt new file mode 100644 index 0000000..d5ed476 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/dropdown/LiquidDropdownMenu.ios.kt @@ -0,0 +1,108 @@ +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.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.UIMenuElementAttributesDisabled +import platform.UIKit.UIMenuElementState + +/** + * 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/field/LiquidSearchField.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt new file mode 100644 index 0000000..f2aec44 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidSearchField.ios.kt @@ -0,0 +1,104 @@ +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..a2ca108 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/field/LiquidTextField.ios.kt @@ -0,0 +1,108 @@ +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.UITextBorderStyle +import platform.UIKit.UITextField +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/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/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/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..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,17 +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() + 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 new file mode 100644 index 0000000..e16bea1 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/picker/LiquidPicker.ios.kt @@ -0,0 +1,119 @@ +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/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..440516f --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/play/LiquidMediaControl.ios.kt @@ -0,0 +1,164 @@ +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/segmented/LiquidSegmentedControl.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/segmented/LiquidSegmentedControl.ios.kt index 163bdf8..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,10 +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.UIControlStateSelected +import platform.UIKit.UIImage import platform.UIKit.UISegmentedControl import platform.darwin.NSObject @@ -45,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) } @@ -70,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, + ), ) } @@ -86,7 +88,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/sheet/LiquidSheet.ios.kt b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt new file mode 100644 index 0000000..4831b51 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/sheet/LiquidSheet.ios.kt @@ -0,0 +1,127 @@ +@file:OptIn(ExperimentalForeignApi::class) + +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 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]. 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 + * 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 glass material for the sheet surface. + * + * 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 { + 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 new file mode 100644 index 0000000..10e31a3 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/stepper/LiquidStepper.ios.kt @@ -0,0 +1,88 @@ +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/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..d4b0dd4 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/surface/LiquidSurface.ios.kt @@ -0,0 +1,99 @@ +@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/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..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 @@ -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 @@ -40,15 +41,17 @@ 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 = { UISwitch().apply { setOn(checked) this.enabled = enabled + onTintColor = style.selectedContentColor.toUIColor() backgroundColor = UIColor.clearColor setOpaque(false) addTarget( @@ -64,12 +67,14 @@ internal actual fun PlatformLiquidToggle( uiSwitch.setOn(checked, animated = true) } uiSwitch.enabled = enabled + 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 new file mode 100644 index 0000000..2ce9979 --- /dev/null +++ b/liquidkit/src/iosMain/kotlin/io/github/androidpoet/liquidkit/toolbar/LiquidToolbar.ios.kt @@ -0,0 +1,110 @@ +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.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/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..d60a70e --- /dev/null +++ b/sampleApp/src/androidMain/kotlin/io/github/androidpoet/liquidkit/sample/MainActivity.kt @@ -0,0 +1,319 @@ +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.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") + 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) { + 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..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 @@ -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,92 +69,117 @@ 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) - } - entry { - LiquidKitSampleTabRoot(selectedTab = LiquidKitSampleTab.Search) + 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.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 = {}, ) { - 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() - .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), ) - 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(onOpenShowcase = onOpenShowcase) + LiquidKitSampleTab.Search -> ControlsSamplePanel() + LiquidKitSampleTab.Settings -> SettingsSamplePanel() } extraContent() @@ -163,15 +192,227 @@ 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()), ) } +/** + * 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) } + 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 = "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", + 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 @@ -181,15 +422,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> = @@ -198,31 +441,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", + ), ), ) } @@ -232,64 +478,203 @@ private fun Header( selectedTab: LiquidKitSampleTab, modifier: Modifier = Modifier, ) { - 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." - } + 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, verticalArrangement = Arrangement.spacedBy(8.dp), ) { BasicText( - text = "LiquidKit", - style = TextStyle( - color = Color(0xFF0F1720), - fontSize = 34.sp, - fontWeight = FontWeight.Bold, - lineHeight = 38.sp, - ), + text = title, + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 34.sp, + fontWeight = FontWeight.Bold, + lineHeight = 38.sp, + ), ) BasicText( text = subtitle, - style = TextStyle( - color = Color(0xFF4A5868), - fontSize = 15.sp, - lineHeight = 21.sp, - ), + style = + TextStyle( + color = Color(0xFF4A4A4A), + fontSize = 15.sp, + lineHeight = 21.sp, + ), ) } } +@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, content: @Composable ColumnScope.() -> Unit, ) { Column( - modifier = modifier - .background( - color = Color(0xCCFFFFFF), - shape = RoundedCornerShape(28.dp), - ) - .border( - width = 1.dp, - color = Color(0x80FFFFFF), - 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, ) } +@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 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, + ), + ) + } +} + @Composable private fun PanelDivider() { Spacer( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .background(Color(0x1F4A5868)), + modifier = + Modifier + .fillMaxWidth() + .height(1.dp) + .background(Color(0x1F000000)), ) } @@ -320,9 +705,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, ) { @@ -347,9 +733,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( @@ -364,11 +751,12 @@ private fun LiquidKitSliderRow( ) BasicText( text = "${(value * 100).toInt()}%", - style = TextStyle( - color = Color(0xFF111820), - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ), ) } LiquidSlider( @@ -388,9 +776,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( @@ -418,27 +807,31 @@ private fun LabelBlock( ) { BasicText( text = label, - style = TextStyle( - color = Color(0xFF111820), - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - ), + style = + TextStyle( + color = Color(0xFF070707), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), ) BasicText( text = description, - style = TextStyle( - color = Color(0xFF43515F), - 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(0xFFEAF5F7), - Color(0xFFF6ECE5), - Color(0xFFE9EDF8), - ), -) +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 new file mode 100644 index 0000000..e103b27 --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ButtonShowcase.kt @@ -0,0 +1,137 @@ +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.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +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, + ) + } + } +} 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..1ccd69e --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/ChromeShowcase.kt @@ -0,0 +1,160 @@ +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, + ), + ) + } + } + } + } +} 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..a761253 --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/FieldShowcase.kt @@ -0,0 +1,112 @@ +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, + ), + ) +} 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..aff75eb --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/MenuShowcase.kt @@ -0,0 +1,163 @@ +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), + ), + ) 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 new file mode 100644 index 0000000..da10f81 --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/StepperMediaShowcase.kt @@ -0,0 +1,115 @@ +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.foundation.text.BasicText +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 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, + ), + ) +} 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..ff38b3c --- /dev/null +++ b/sampleApp/src/commonMain/kotlin/io/github/androidpoet/liquidkit/sample/showcase/SurfaceShowcase.kt @@ -0,0 +1,162 @@ +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), + ), + ) 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..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 @@ -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 { + LiquidKitSampleTabScreen(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' } + }, +}