diff --git a/appyx-components/experimental/cards/common/build.gradle.kts b/appyx-components/experimental/cards/common/build.gradle.kts
index 054ebae60..dc1a08eb0 100644
--- a/appyx-components/experimental/cards/common/build.gradle.kts
+++ b/appyx-components/experimental/cards/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -24,6 +26,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-components-experimental-cards-commons"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-components-experimental-cards-commons-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
iosArm64()
@@ -56,11 +74,16 @@ kotlin {
}
}
+compose.experimental {
+ web.application {}
+}
+
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspAndroid", project(":ksp:appyx-processor"))
add("kspDesktop", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
add("kspIosArm64", project(":ksp:appyx-processor"))
add("kspIosX64", project(":ksp:appyx-processor"))
add("kspIosSimulatorArm64", project(":ksp:appyx-processor"))
diff --git a/appyx-components/experimental/cards/common/karma.config.d/wasm/config.js b/appyx-components/experimental/cards/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/appyx-components/experimental/cards/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/appyx-components/experimental/promoter/common/build.gradle.kts b/appyx-components/experimental/promoter/common/build.gradle.kts
index 10f2e7458..54ffbbef3 100644
--- a/appyx-components/experimental/promoter/common/build.gradle.kts
+++ b/appyx-components/experimental/promoter/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -24,6 +26,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-components-experimental-promoter-commons"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-components-experimental-promoter-commons-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
iosArm64()
@@ -57,11 +75,16 @@ kotlin {
}
}
+compose.experimental {
+ web.application {}
+}
+
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspAndroid", project(":ksp:appyx-processor"))
add("kspDesktop", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
add("kspIosArm64", project(":ksp:appyx-processor"))
add("kspIosX64", project(":ksp:appyx-processor"))
add("kspIosSimulatorArm64", project(":ksp:appyx-processor"))
diff --git a/appyx-components/experimental/promoter/common/karma.config.d/wasm/config.js b/appyx-components/experimental/promoter/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/appyx-components/experimental/promoter/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/appyx-components/experimental/puzzle15/common/build.gradle.kts b/appyx-components/experimental/puzzle15/common/build.gradle.kts
index 6b484b3bc..6a8a08133 100644
--- a/appyx-components/experimental/puzzle15/common/build.gradle.kts
+++ b/appyx-components/experimental/puzzle15/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -24,6 +26,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-components-experimental-puzzle15-commons"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-components-experimental-puzzle15-commons-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
sourceSets {
val commonMain by getting {
@@ -44,9 +62,14 @@ kotlin {
}
}
+compose.experimental {
+ web.application {}
+}
+
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspAndroid", project(":ksp:appyx-processor"))
add("kspDesktop", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/appyx-components/experimental/puzzle15/common/karma.config.d/wasm/config.js b/appyx-components/experimental/puzzle15/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/appyx-components/experimental/puzzle15/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/appyx-components/experimental/puzzle15/web/build.gradle.kts b/appyx-components/experimental/puzzle15/web/build.gradle.kts
index 332d30812..ef97d2bbe 100644
--- a/appyx-components/experimental/puzzle15/web/build.gradle.kts
+++ b/appyx-components/experimental/puzzle15/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -9,6 +11,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-components-experimental-puzzle15-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
diff --git a/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js b/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/experimental/puzzle15/web/main.js.kt b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/experimental/puzzle15/web/main.js.kt
new file mode 100644
index 000000000..4adc93d61
--- /dev/null
+++ b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/experimental/puzzle15/web/main.js.kt
@@ -0,0 +1,51 @@
+package com.bumble.appyx.experimental.puzzle15.web
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Surface
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.components.experimental.puzzle15.ui.Puzzle15Ui
+
+external fun onWasmReady(onReady: () -> Unit)
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ onWasmReady {
+ CanvasBasedWindow("Puzzle15") {
+ val requester = remember { FocusRequester() }
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ Puzzle15Ui(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black)
+ .focusRequester(requester)
+ .focusable(),
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ requester.requestFocus()
+ }
+ }
+ }
+}
diff --git a/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..5d3cb39cc
--- /dev/null
+++ b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Puzzle15
+
+
+
+
+
+
+
+
+
+
diff --git a/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..8655f2e76
--- /dev/null
+++ b/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,12 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
diff --git a/appyx-components/internal/test-drive/common/build.gradle.kts b/appyx-components/internal/test-drive/common/build.gradle.kts
index 926510668..6be5ae252 100644
--- a/appyx-components/internal/test-drive/common/build.gradle.kts
+++ b/appyx-components/internal/test-drive/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -23,6 +25,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-components-internal-testdrive-common"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-components-internal-testdrive-common-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
sourceSets {
val commonMain by getting {
@@ -44,9 +62,14 @@ kotlin {
}
}
+compose.experimental {
+ web.application {}
+}
+
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspAndroid", project(":ksp:appyx-processor"))
add("kspDesktop", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/appyx-components/internal/test-drive/common/karma.config.d/wasm/config.js b/appyx-components/internal/test-drive/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/appyx-components/internal/test-drive/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/appyx-components/standard/backstack/common/build.gradle.kts b/appyx-components/standard/backstack/common/build.gradle.kts
index ae4cd61cd..195cc2bb2 100644
--- a/appyx-components/standard/backstack/common/build.gradle.kts
+++ b/appyx-components/standard/backstack/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -24,6 +26,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-components-stable-backstack-commons"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-components-stable-backstack-commons-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
@@ -47,6 +65,7 @@ kotlin {
val androidMain by getting
val desktopMain by getting
val jsMain by getting
+ val wasmJsMain by getting
val iosX64Main by getting
val iosArm64Main by getting
@@ -60,11 +79,16 @@ kotlin {
}
}
+compose.experimental {
+ web.application {}
+}
+
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspAndroid", project(":ksp:appyx-processor"))
add("kspDesktop", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
add("kspIosArm64", project(":ksp:appyx-processor"))
add("kspIosX64", project(":ksp:appyx-processor"))
add("kspIosSimulatorArm64", project(":ksp:appyx-processor"))
diff --git a/appyx-components/standard/backstack/common/karma.config.d/wasm/config.js b/appyx-components/standard/backstack/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/appyx-components/standard/backstack/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/appyx-components/standard/spotlight/common/build.gradle.kts b/appyx-components/standard/spotlight/common/build.gradle.kts
index 2b56f8952..5942cad0e 100644
--- a/appyx-components/standard/spotlight/common/build.gradle.kts
+++ b/appyx-components/standard/spotlight/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -24,6 +26,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-components-stable-spotlight-commons"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-components-stable-spotlight-commons-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
@@ -63,11 +81,16 @@ kotlin {
}
}
+compose.experimental {
+ web.application {}
+}
+
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspAndroid", project(":ksp:appyx-processor"))
add("kspDesktop", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
add("kspIosArm64", project(":ksp:appyx-processor"))
add("kspIosX64", project(":ksp:appyx-processor"))
add("kspIosSimulatorArm64", project(":ksp:appyx-processor"))
diff --git a/appyx-components/standard/spotlight/common/karma.config.d/wasm/config.js b/appyx-components/standard/spotlight/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/appyx-components/standard/spotlight/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/appyx-interactions/common/build.gradle.kts b/appyx-interactions/common/build.gradle.kts
index 72ee1489e..effc21525 100644
--- a/appyx-interactions/common/build.gradle.kts
+++ b/appyx-interactions/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
kotlin("plugin.serialization")
@@ -24,6 +26,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-interactions-common"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-interactions-common-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
@@ -79,3 +97,7 @@ kotlin {
}
}
}
+
+compose.experimental {
+ web.application {}
+}
diff --git a/appyx-interactions/common/karma.config.d/wasm/config.js b/appyx-interactions/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/appyx-interactions/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/appyx-interactions/common/src/jsMain/kotlin/com/bumble/appyx/interactions/platform.kt b/appyx-interactions/common/src/jsMain/kotlin/com/bumble/appyx/interactions/platform.kt
index b66bfb4e3..ba9d2071b 100644
--- a/appyx-interactions/common/src/jsMain/kotlin/com/bumble/appyx/interactions/platform.kt
+++ b/appyx-interactions/common/src/jsMain/kotlin/com/bumble/appyx/interactions/platform.kt
@@ -1,3 +1,3 @@
package com.bumble.appyx.interactions
-actual fun getPlatformName(): String = "Web"
+actual fun getPlatformName(): String = "Kotlin/JS"
diff --git a/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/SystemClock.kt b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/SystemClock.kt
new file mode 100644
index 000000000..90b265262
--- /dev/null
+++ b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/SystemClock.kt
@@ -0,0 +1,13 @@
+package com.bumble.appyx.interactions
+
+import kotlinx.browser.window
+import kotlin.math.roundToLong
+
+actual object SystemClock {
+
+ actual fun nanoTime(): Long =
+ (window.performance.now() * MillisToNanos).roundToLong()
+
+ private const val MillisToNanos = 1_000_000L
+
+}
diff --git a/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/UUID.kt b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/UUID.kt
new file mode 100644
index 000000000..e3ae0ca8b
--- /dev/null
+++ b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/UUID.kt
@@ -0,0 +1,13 @@
+package com.bumble.appyx.interactions
+
+@JsModule("uuid")
+private external object Uuid {
+ fun v4(): String
+}
+
+actual object UUID {
+
+ actual fun randomUUID(): String =
+ Uuid.v4()
+
+}
diff --git a/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/platform.kt b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/platform.kt
new file mode 100644
index 000000000..a155aefee
--- /dev/null
+++ b/appyx-interactions/common/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/platform.kt
@@ -0,0 +1,3 @@
+package com.bumble.appyx.interactions
+
+actual fun getPlatformName(): String = "Kotlin/Wasm"
diff --git a/appyx-navigation/common/build.gradle.kts b/appyx-navigation/common/build.gradle.kts
index 11c1cc48c..4b5df9db5 100644
--- a/appyx-navigation/common/build.gradle.kts
+++ b/appyx-navigation/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -9,6 +11,7 @@ appyx {
androidNamespace.set("com.bumble.appyx.navigation")
}
+@OptIn(ExperimentalWasmDsl::class)
kotlin {
androidTarget {
publishLibraryVariants("release")
@@ -22,6 +25,21 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-navigation-common"
browser()
+ binaries.executable()
+ }
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-navigation-common-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
@@ -98,3 +116,7 @@ android {
androidTestImplementation(project(":utils:testing-ui"))
}
}
+
+compose.experimental {
+ web.application {}
+}
diff --git a/appyx-navigation/common/karma.config.d/wasm/config.js b/appyx-navigation/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/appyx-navigation/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/appyx-navigation/common/src/jsMain/kotlin/com/bumble/appyx/navigation/integration/BrowserViewportWindow.kt b/appyx-navigation/common/src/jsMain/kotlin/com/bumble/appyx/navigation/integration/BrowserViewportWindow.kt
deleted file mode 100644
index 90f94b819..000000000
--- a/appyx-navigation/common/src/jsMain/kotlin/com/bumble/appyx/navigation/integration/BrowserViewportWindow.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-// From Slack by OliverO
-// See: https://kotlinlang.slack.com/archives/C01F2HV7868/p1660083429206369?thread_ts=1660083398.571449&cid=C01F2HV7868
-// adapted with scaling fix from https://github.com/OliverO2/compose-counting-grid/blob/master/src/frontendJsMain/kotlin/BrowserViewportWindow.kt
-
-@file:Suppress(
- "INVISIBLE_MEMBER",
- "INVISIBLE_REFERENCE",
- "EXPOSED_PARAMETER_TYPE"
-) // WORKAROUND: ComposeWindow and ComposeLayer are internal
-
-package com.bumble.appyx.navigation.integration
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.window.ComposeWindow
-import kotlinx.browser.document
-import kotlinx.browser.window
-import org.w3c.dom.HTMLCanvasElement
-import org.w3c.dom.HTMLStyleElement
-import org.w3c.dom.HTMLTitleElement
-
-private const val CANVAS_ELEMENT_ID = "ComposeTarget" // Hardwired into ComposeWindow
-
-/**
- * A Skiko/Canvas-based top-level window using the browser's entire viewport. Supports resizing.
- */
-@Composable
-@Suppress("FunctionNaming")
-fun BrowserViewportWindow(
- title: String = "Untitled",
- content: @Composable () -> Unit
-) {
- val htmlHeadElement = document.head!!
- htmlHeadElement.appendChild(
- (document.createElement("style") as HTMLStyleElement).apply {
- type = "text/css"
- appendChild(
- document.createTextNode(
- """
- html, body {
- overflow: hidden;
- margin: 0 !important;
- padding: 0 !important;
- }
-
- #$CANVAS_ELEMENT_ID {
- outline: none;
- }
- """.trimIndent()
- )
- )
- }
- )
-
- fun HTMLCanvasElement.fillViewportSize() {
- setAttribute("width", "${window.innerWidth}")
- setAttribute("height", "${window.innerHeight}")
- }
-
- val canvas = (document.getElementById(CANVAS_ELEMENT_ID) as HTMLCanvasElement).apply {
- fillViewportSize()
- }
-
- ComposeWindow(canvasId = "Appyx", content = content).apply {
- window.addEventListener("resize", {
- canvas.fillViewportSize()
- layer.layer.attachTo(canvas)
- layer.layer.needRedraw()
- val scale = layer.layer.contentScale
- layer.setSize(
- (canvas.width / scale * density.density).toInt(),
- (canvas.height / scale * density.density).toInt()
- )
- })
-
- // WORKAROUND: ComposeWindow does not implement `setTitle(title)`
- val htmlTitleElement = (
- htmlHeadElement.getElementsByTagName("title").item(0)
- ?: document.createElement("title").also { htmlHeadElement.appendChild(it) }
- ) as HTMLTitleElement
- htmlTitleElement.textContent = title
- }
-}
diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/MainIntegrationPoint.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/MainIntegrationPoint.kt
new file mode 100644
index 000000000..2adb723bb
--- /dev/null
+++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/MainIntegrationPoint.kt
@@ -0,0 +1,16 @@
+package com.bumble.appyx.navigation.integration
+
+
+class MainIntegrationPoint : IntegrationPoint() {
+ override val isChangingConfigurations: Boolean
+ get() = false
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onRootFinished() {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun handleUpNavigation() {
+
+ }
+}
diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/WebNodeHost.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/WebNodeHost.kt
new file mode 100644
index 000000000..b2b2b30d4
--- /dev/null
+++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/integration/WebNodeHost.kt
@@ -0,0 +1,65 @@
+package com.bumble.appyx.navigation.integration
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.navigation.node.Node
+import com.bumble.appyx.navigation.platform.LocalOnBackPressedDispatcherOwner
+import com.bumble.appyx.navigation.platform.OnBackPressedDispatcher
+import com.bumble.appyx.navigation.platform.OnBackPressedDispatcherOwner
+import com.bumble.appyx.navigation.platform.PlatformLifecycleRegistry
+import com.bumble.appyx.utils.customisations.NodeCustomisationDirectory
+import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+
+/**
+ * Composable function to host [Node<*>].
+ *
+ * This convenience wrapper provides an [OnBackPressedDispatcherOwner] hooked up to the
+ * [.onBackPressedEvents] flow to simplify implementing the global "go back" functionality
+ * that is a common concept in the Appyx framework.
+ */
+@Suppress("ComposableParamOrder")
+@Composable
+fun > WebNodeHost(
+ screenSize: ScreenSize,
+ onBackPressedEvents: Flow,
+ modifier: Modifier = Modifier,
+ integrationPoint: IntegrationPoint = remember { MainIntegrationPoint() },
+ customisations: NodeCustomisationDirectory = remember { NodeCustomisationDirectoryImpl() },
+ factory: NodeFactory
+) {
+ val platformLifecycleRegistry = remember {
+ PlatformLifecycleRegistry()
+ }
+ val onBackPressedDispatcherOwner = remember {
+ object : OnBackPressedDispatcherOwner {
+ override val onBackPressedDispatcher: OnBackPressedDispatcher =
+ OnBackPressedDispatcher { integrationPoint.handleUpNavigation() }
+ }
+ }
+
+ val scope = rememberCoroutineScope()
+ LaunchedEffect(onBackPressedEvents) {
+ scope.launch {
+ onBackPressedEvents.collect {
+ onBackPressedDispatcherOwner.onBackPressedDispatcher.onBackPressed()
+ }
+ }
+ }
+
+ CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides onBackPressedDispatcherOwner) {
+ NodeHost(
+ lifecycle = platformLifecycleRegistry,
+ integrationPoint = integrationPoint,
+ modifier = modifier,
+ customisations = customisations,
+ screenSize = screenSize,
+ factory = factory,
+ )
+ }
+}
diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedCallback.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedCallback.kt
new file mode 100644
index 000000000..a568631a5
--- /dev/null
+++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedCallback.kt
@@ -0,0 +1,59 @@
+package com.bumble.appyx.navigation.platform
+
+interface Cancellable {
+ /**
+ * Cancel the subscription. This call should be idempotent, making it safe to
+ * call multiple times.
+ */
+ fun cancel()
+}
+
+/**
+ * Create a [OnBackPressedCallback].
+ *
+ * @param isEnabled The default enabled state for this callback.
+ * @see .setEnabled
+ */
+abstract class OnBackPressedCallback(
+ /**
+ * Set the enabled state of the callback. Only when this callback
+ * is enabled will it receive callbacks to [.handleOnBackPressed].
+ *
+ * @param isEnabled whether the callback should be considered enabled
+ */
+ var isEnabled: Boolean
+) {
+ /**
+ * Checks whether this callback should be considered enabled. Only when this callback
+ * is enabled will it receive callbacks to [.handleOnBackPressed].
+ *
+ * @return Whether this callback should be considered enabled.
+ */
+ private val cancellables: MutableList = mutableListOf()
+
+ /**
+ * Removes this callback from any [OnBackPressedDispatcher] it is currently
+ * added to.
+ */
+ fun remove() {
+ for (cancellable in cancellables) {
+ cancellable.cancel()
+ }
+ }
+
+ /**
+ * Callback for handling the [OnBackPressedDispatcher.onBackPressed] event.
+ */
+ abstract fun handleOnBackPressed()
+ fun addCancellable(cancellable: Cancellable) {
+ run {
+ cancellables.add(cancellable)
+ }
+ }
+
+ fun removeCancellable(cancellable: Cancellable) {
+ run {
+ cancellables.remove(cancellable)
+ }
+ }
+}
diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedDispatcher.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedDispatcher.kt
new file mode 100644
index 000000000..0bf083749
--- /dev/null
+++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/OnBackPressedDispatcher.kt
@@ -0,0 +1,69 @@
+package com.bumble.appyx.navigation.platform
+
+import androidx.compose.ui.util.fastForEachReversed
+
+/**
+ * Adapted from Android's OnBackPressedDispatcher.
+ *
+ * Create a new OnBackPressedDispatcher that dispatches [.onBackPressed] events
+ * to one or more [OnBackPressedCallback] instances.
+ *
+ * @param fallbackOnBackPressed The Runnable that should be triggered if
+ * [.onBackPressed] is called when no [OnBackPressedCallback] have been registered.
+ */
+class OnBackPressedDispatcher(private val fallbackOnBackPressed: (() -> Unit)? = null) {
+ val onBackPressedCallbacks: ArrayDeque =
+ ArrayDeque()
+
+ /**
+ * Internal implementation of [.addCallback] that gives
+ * access to the [Cancellable] that specifically removes this callback from
+ * the dispatcher without relying on [OnBackPressedCallback.remove] which
+ * is what external developers should be using.
+ *
+ * @param onBackPressedCallback The callback to add
+ * @return a [Cancellable] which can be used to [cancel][Cancellable.cancel]
+ * the callback and remove it from the set of OnBackPressedCallbacks.
+ */
+ fun addCancellableCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable {
+ onBackPressedCallbacks.add(onBackPressedCallback)
+ val cancellable = OnBackPressedCancellable(onBackPressedCallback)
+ onBackPressedCallback.addCancellable(cancellable)
+ return cancellable
+ }
+
+ /**
+ * Trigger a call to the currently added [callbacks][OnBackPressedCallback] in reverse
+ * order in which they were added. Only if the most recently added callback is not
+ * [enabled][OnBackPressedCallback.isEnabled]
+ * will any previously added callback be called.
+ *
+ *
+ * If [.hasEnabledCallbacks] is `false` when this method is called, the
+ * fallback Runnable set by [the constructor][.OnBackPressedDispatcher]
+ * will be triggered.
+ */
+ fun onBackPressed() {
+ onBackPressedCallbacks.fastForEachReversed { callback ->
+ if (callback.isEnabled) {
+ callback.handleOnBackPressed()
+ return
+ }
+ }
+ fallbackOnBackPressed?.invoke()
+ }
+
+ private inner class OnBackPressedCancellable(onBackPressedCallback: OnBackPressedCallback) :
+ Cancellable {
+ private val onBackPressedCallback: OnBackPressedCallback
+
+ init {
+ this.onBackPressedCallback = onBackPressedCallback
+ }
+
+ override fun cancel() {
+ onBackPressedCallbacks.remove(onBackPressedCallback)
+ onBackPressedCallback.removeCancellable(this)
+ }
+ }
+}
diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformBackHandler.wasmJs.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformBackHandler.wasmJs.kt
new file mode 100644
index 000000000..46113be27
--- /dev/null
+++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformBackHandler.wasmJs.kt
@@ -0,0 +1,56 @@
+package com.bumble.appyx.navigation.platform
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+
+@Suppress("CompositionLocalAllowlist")
+val LocalOnBackPressedDispatcherOwner: ProvidableCompositionLocal =
+ compositionLocalOf {
+ object : OnBackPressedDispatcherOwner {
+ override val onBackPressedDispatcher: OnBackPressedDispatcher
+ get() = OnBackPressedDispatcher(null)
+ }
+ }
+
+interface OnBackPressedDispatcherOwner {
+ val onBackPressedDispatcher: OnBackPressedDispatcher
+}
+
+@Composable
+actual fun PlatformBackHandler(
+ enabled: Boolean,
+ onBack: () -> Unit
+) {
+ // Safely update the current `onBack` lambda when a new one is provided
+ val currentOnBack by rememberUpdatedState(onBack)
+ // Remember in Composition a back callback that calls the `onBack` lambda
+ val backCallback = remember {
+ object : OnBackPressedCallback(enabled) {
+ override fun handleOnBackPressed() {
+ currentOnBack()
+ }
+ }
+ }
+ // On every successful composition, update the callback with the `enabled` value
+ SideEffect {
+ backCallback.isEnabled = enabled
+ }
+
+ // register for back events only whilst present in the composition
+ val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
+ "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
+ }.onBackPressedDispatcher
+ DisposableEffect(backDispatcher) {
+ val cancellable = backDispatcher.addCancellableCallback(backCallback)
+
+ onDispose {
+ cancellable.cancel()
+ }
+ }
+}
diff --git a/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformLifecycleRegistry.kt b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformLifecycleRegistry.kt
new file mode 100644
index 000000000..518ca5afa
--- /dev/null
+++ b/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/platform/PlatformLifecycleRegistry.kt
@@ -0,0 +1,91 @@
+package com.bumble.appyx.navigation.platform
+
+import com.bumble.appyx.navigation.lifecycle.CommonLifecycleOwner
+import com.bumble.appyx.navigation.lifecycle.DefaultPlatformLifecycleObserver
+import com.bumble.appyx.navigation.lifecycle.Lifecycle
+import com.bumble.appyx.navigation.lifecycle.PlatformLifecycleEventObserver
+import com.bumble.appyx.navigation.lifecycle.PlatformLifecycleObserver
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.isActive
+
+actual class PlatformLifecycleRegistry : Lifecycle {
+
+ private val managedDefaultLifecycleObservers: MutableList =
+ ArrayList()
+ private val managedLifecycleEventObservers: MutableList =
+ ArrayList()
+
+ private var _currentState: Lifecycle.State = Lifecycle.State.INITIALIZED
+ override var currentState: Lifecycle.State
+ get() = _currentState
+ set(value) {
+ when (value) {
+ Lifecycle.State.INITIALIZED -> Unit
+ Lifecycle.State.CREATED -> {
+ managedDefaultLifecycleObservers.forEach { it.onCreate() }
+ managedLifecycleEventObservers.forEach {
+ it.onStateChanged(
+ value,
+ Lifecycle.Event.ON_CREATE
+ )
+ }
+ }
+ Lifecycle.State.STARTED -> {
+ managedDefaultLifecycleObservers.forEach { it.onStart() }
+ managedLifecycleEventObservers.forEach {
+ it.onStateChanged(
+ value,
+ Lifecycle.Event.ON_START
+ )
+ }
+ }
+ Lifecycle.State.RESUMED -> {
+ managedDefaultLifecycleObservers.forEach { it.onResume() }
+ managedLifecycleEventObservers.forEach {
+ it.onStateChanged(
+ value,
+ Lifecycle.Event.ON_RESUME
+ )
+ }
+ }
+ Lifecycle.State.DESTROYED -> {
+ managedDefaultLifecycleObservers.forEach { it.onDestroy() }
+ managedLifecycleEventObservers.forEach {
+ it.onStateChanged(
+ value,
+ Lifecycle.Event.ON_DESTROY
+ )
+ }
+ if (coroutineScope.isActive) coroutineScope.cancel("lifecycle was destroyed")
+ }
+ }
+ _currentState = value
+ }
+
+ override val coroutineScope: CoroutineScope by lazy { MainScope() }
+
+ override fun addObserver(observer: PlatformLifecycleObserver) {
+ when (observer) {
+ is DefaultPlatformLifecycleObserver -> managedDefaultLifecycleObservers.add(observer)
+ is PlatformLifecycleEventObserver -> managedLifecycleEventObservers.add(observer)
+ }
+ }
+
+ override fun removeObserver(observer: PlatformLifecycleObserver) {
+ when (observer) {
+ is DefaultPlatformLifecycleObserver -> managedDefaultLifecycleObservers.remove(observer)
+ is PlatformLifecycleEventObserver -> managedLifecycleEventObservers.remove(observer)
+ }
+ }
+
+ actual fun setCurrentState(state: Lifecycle.State) {
+ currentState = state
+ }
+
+ actual companion object {
+ actual fun create(owner: CommonLifecycleOwner): PlatformLifecycleRegistry =
+ PlatformLifecycleRegistry()
+ }
+}
diff --git a/demos/appyx-interactions/web/build.gradle.kts b/demos/appyx-interactions/web/build.gradle.kts
index ba3d15648..b0036472e 100644
--- a/demos/appyx-interactions/web/build.gradle.kts
+++ b/demos/appyx-interactions/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -9,6 +11,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-demos-interactions-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
diff --git a/demos/appyx-interactions/web/karma.config.d/wasm/config.js b/demos/appyx-interactions/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/appyx-interactions/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/appyx-interactions/web/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/main.js.kt b/demos/appyx-interactions/web/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/main.js.kt
new file mode 100644
index 000000000..34426f6f8
--- /dev/null
+++ b/demos/appyx-interactions/web/src/wasmJsMain/kotlin/com/bumble/appyx/interactions/main.js.kt
@@ -0,0 +1,79 @@
+package com.bumble.appyx.interactions
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.components.internal.testdrive.TestDriveExperiment
+import com.bumble.appyx.components.internal.testdrive.ui.md_amber_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_blue_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_blue_grey_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_cyan_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_grey_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_indigo_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_light_blue_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_light_green_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_lime_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_pink_500
+import com.bumble.appyx.components.internal.testdrive.ui.md_teal_500
+
+external fun onWasmReady(onReady: () -> Unit)
+
+val manatee = Color(0xFF8D99AE)
+val silver_sand = Color(0xFFBDC6D1)
+val sizzling_red = Color(0xFFF05D5E)
+val atomic_tangerine = Color(0xFFF0965D)
+
+val colors = listOf(
+ manatee,
+ sizzling_red,
+ atomic_tangerine,
+ silver_sand,
+ md_pink_500,
+ md_indigo_500,
+ md_blue_500,
+ md_light_blue_500,
+ md_cyan_500,
+ md_teal_500,
+ md_light_green_500,
+ md_lime_500,
+ md_amber_500,
+ md_grey_500,
+ md_blue_grey_500
+)
+
+enum class InteractionTarget {
+ Child1
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ onWasmReady {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ TestDriveExperiment(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ element = InteractionTarget.Child1,
+ modifier = Modifier.fillMaxSize().background(Color.Black),
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/appyx-interactions/web/src/wasmJsMain/resources/index.html b/demos/appyx-interactions/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..834b74027
--- /dev/null
+++ b/demos/appyx-interactions/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/appyx-interactions/web/src/wasmJsMain/resources/styles.css b/demos/appyx-interactions/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..8655f2e76
--- /dev/null
+++ b/demos/appyx-interactions/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,12 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
diff --git a/demos/appyx-navigation/common/build.gradle.kts b/demos/appyx-navigation/common/build.gradle.kts
index 85c316443..9e77e0a02 100644
--- a/demos/appyx-navigation/common/build.gradle.kts
+++ b/demos/appyx-navigation/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -23,6 +25,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "demo-appyx-navigation-common"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "demo-appyx-navigation-common-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
iosArm64()
@@ -90,11 +108,16 @@ android {
}
}
+compose.experimental {
+ web.application {}
+}
+
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspAndroid", project(":ksp:appyx-processor"))
add("kspDesktop", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
add("kspIosArm64", project(":ksp:appyx-processor"))
add("kspIosX64", project(":ksp:appyx-processor"))
add("kspIosSimulatorArm64", project(":ksp:appyx-processor"))
diff --git a/demos/appyx-navigation/common/karma.config.d/wasm/config.js b/demos/appyx-navigation/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/appyx-navigation/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/platform/PlatformName.kt b/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/platform/PlatformName.kt
new file mode 100644
index 000000000..ff4c7405d
--- /dev/null
+++ b/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/platform/PlatformName.kt
@@ -0,0 +1,3 @@
+package com.bumble.appyx.demos.navigation.platform
+
+actual fun getPlatformName(): String = "Kotlin/Wasm"
diff --git a/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/ui/EmbeddableResourceImage.kt b/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/ui/EmbeddableResourceImage.kt
new file mode 100644
index 000000000..32c3edfc2
--- /dev/null
+++ b/demos/appyx-navigation/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/navigation/ui/EmbeddableResourceImage.kt
@@ -0,0 +1,24 @@
+package com.bumble.appyx.demos.navigation.ui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import com.bumble.appyx.imageloader.ResourceImage
+
+private const val EMBED_URL_PATH = "appyx/samples/documentation-appyx-navigation/"
+
+@Composable
+actual fun EmbeddableResourceImage(
+ path: String,
+ contentDescription: String,
+ contentScale: ContentScale,
+ modifier: Modifier
+) {
+ ResourceImage(
+ path = EMBED_URL_PATH + path,
+ fallbackUrl = path,
+ contentDescription = contentDescription,
+ contentScale = contentScale,
+ modifier = modifier,
+ )
+}
diff --git a/demos/appyx-navigation/web/build.gradle.kts b/demos/appyx-navigation/web/build.gradle.kts
index d4fa3ec95..668b1c58c 100644
--- a/demos/appyx-navigation/web/build.gradle.kts
+++ b/demos/appyx-navigation/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-demos-navigation-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,22 +52,41 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
-tasks.register("copyResources") {
+tasks.register("jsCopyResources") {
// Dirs containing files we want to copy
from("../common/src/commonMain/resources")
// Output for web resources
- into("$buildDir/processedResources/js/main")
+ into("${layout.buildDirectory}/processedResources/js/main")
include("**/*")
}
tasks.named("jsBrowserProductionExecutableDistributeResources") {
- dependsOn("copyResources")
+ dependsOn("jsCopyResources")
}
tasks.named("jsMainClasses") {
- dependsOn("copyResources")
+ dependsOn("jsCopyResources")
+}
+
+tasks.register("wasmJsCopyResources") {
+ // Dirs containing files we want to copy
+ from("../common/src/commonMain/resources")
+
+ // Output for web resources
+ into("${layout.buildDirectory}/processedResources/wasmJs/main")
+
+ include("**/*")
+}
+
+tasks.named("wasmJsBrowserProductionExecutableDistributeResources") {
+ dependsOn("wasmJsCopyResources")
}
+
+tasks.named("wasmJsMainClasses") {
+ dependsOn("wasmJsCopyResources")
+}
\ No newline at end of file
diff --git a/demos/appyx-navigation/web/karma.config.d/wasm/config.js b/demos/appyx-navigation/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/appyx-navigation/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/Main.kt b/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/Main.kt
new file mode 100644
index 000000000..e43f5ab47
--- /dev/null
+++ b/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/Main.kt
@@ -0,0 +1,138 @@
+package com.bumble.appyx.navigation
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+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.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_primary
+import com.bumble.appyx.demos.navigation.navigator.LocalNavigator
+import com.bumble.appyx.demos.navigation.navigator.Navigator
+import com.bumble.appyx.demos.navigation.node.root.RootNode
+import com.bumble.appyx.demos.navigation.ui.AppyxSampleAppTheme
+import com.bumble.appyx.navigation.integration.ScreenSize
+import com.bumble.appyx.navigation.integration.WebNodeHost
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ val events: Channel = Channel()
+ val navigator = Navigator()
+ appyxSample {
+ CanvasBasedWindow("Appyx navigation demo") {
+ CakeApp(events, navigator)
+ }
+ }
+}
+
+private val containerShape = RoundedCornerShape(8)
+
+@Composable
+private fun CakeApp(events: Channel, navigator: Navigator) {
+ AppyxSampleAppTheme(darkTheme = true, themeTypography = webTypography) {
+ val requester = remember { FocusRequester() }
+ var hasFocus by remember { mutableStateOf(false) }
+
+ var screenSize by remember { mutableStateOf(ScreenSize(0.dp, 0.dp)) }
+ val eventScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
+
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { screenSize = ScreenSize(it.width.dp, it.height.dp) }
+ .onKeyEvent {
+ onKeyEvent(it, events, eventScope)
+ }
+ .focusRequester(requester)
+ .focusable()
+ .onFocusChanged { hasFocus = it.hasFocus },
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ CompositionLocalProvider(LocalNavigator provides navigator) {
+ BlackContainer {
+ WebNodeHost(
+ screenSize = screenSize,
+ onBackPressedEvents = events.receiveAsFlow(),
+ ) { nodeContext ->
+ RootNode(
+ nodeContext = nodeContext,
+ plugins = listOf(navigator)
+ )
+ }
+ }
+ }
+ }
+
+ if (!hasFocus) {
+ LaunchedEffect(Unit) {
+ requester.requestFocus()
+ }
+ }
+ }
+}
+
+@Composable
+private fun BlackContainer(content: @Composable () -> Unit) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .aspectRatio(0.56f)
+ .border(4.dp, color_primary, containerShape)
+ .clip(containerShape)
+ ) {
+ content()
+ }
+ }
+}
+
+private fun onKeyEvent(
+ keyEvent: KeyEvent,
+ events: Channel,
+ coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
+): Boolean =
+ when {
+ keyEvent.type == KeyEventType.KeyUp && keyEvent.key == Key.Backspace -> {
+ coroutineScope.launch { events.send(Unit) }
+ true
+ }
+
+ else -> false
+ }
diff --git a/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/WebTypography.kt b/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/WebTypography.kt
new file mode 100644
index 000000000..833b55cac
--- /dev/null
+++ b/demos/appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/navigation/WebTypography.kt
@@ -0,0 +1,44 @@
+package com.bumble.appyx.navigation
+
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.sp
+import com.bumble.appyx.demos.navigation.ui.typography
+
+internal val webTypography = typography.copy(
+ bodySmall = typography.bodySmall.copy(
+ fontSize = 8.sp,
+ fontFamily = FontFamily.SansSerif,
+ ),
+ bodyMedium = typography.bodyMedium.copy(
+ fontSize = 10.sp,
+ fontFamily = FontFamily.SansSerif,
+ ),
+ bodyLarge = typography.bodyLarge.copy(
+ fontSize = 12.sp,
+ fontFamily = FontFamily.SansSerif,
+ ),
+ titleSmall = typography.titleSmall.copy(
+ fontSize = 8.sp,
+ fontFamily = FontFamily.SansSerif,
+ ),
+ titleMedium = typography.titleMedium.copy(
+ fontSize = 10.sp,
+ fontFamily = FontFamily.SansSerif,
+ ),
+ titleLarge = typography.titleLarge.copy(
+ fontSize = 12.sp,
+ fontFamily = FontFamily.SansSerif,
+ ),
+ headlineSmall = typography.headlineSmall.copy(
+ fontSize = 14.sp,
+ fontFamily = FontFamily.SansSerif,
+ ),
+ headlineMedium = typography.headlineMedium.copy(
+ fontSize = 16.sp,
+ fontFamily = FontFamily.SansSerif,
+ ),
+ headlineLarge = typography.headlineLarge.copy(
+ fontSize = 18.sp,
+ fontFamily = FontFamily.SansSerif,
+ ),
+)
diff --git a/demos/appyx-navigation/web/src/wasmJsMain/resources/index.html b/demos/appyx-navigation/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..07c3ca9bc
--- /dev/null
+++ b/demos/appyx-navigation/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Navigation Demo
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/appyx-navigation/web/src/wasmJsMain/resources/styles.css b/demos/appyx-navigation/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..8655f2e76
--- /dev/null
+++ b/demos/appyx-navigation/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,12 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
diff --git a/demos/common/build.gradle.kts b/demos/common/build.gradle.kts
index abcbffc86..2d6862b5f 100644
--- a/demos/common/build.gradle.kts
+++ b/demos/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -22,6 +24,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-demos-commons"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-demos-commons-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
iosArm64()
@@ -61,3 +79,7 @@ kotlin {
}
}
}
+
+compose.experimental {
+ web.application {}
+}
diff --git a/demos/common/karma.config.d/wasm/config.js b/demos/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/image-loader/common/build.gradle.kts b/demos/image-loader/common/build.gradle.kts
index b8d70eb27..81da378f0 100644
--- a/demos/image-loader/common/build.gradle.kts
+++ b/demos/image-loader/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -21,6 +23,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-navigation-imageloader"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-navigation-imageloader-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
@@ -87,3 +105,7 @@ android {
androidTestImplementation(composeBom)
}
}
+
+compose.experimental {
+ web.application {}
+}
diff --git a/demos/image-loader/common/karma.config.d/wasm/config.js b/demos/image-loader/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/image-loader/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/image-loader/common/src/wasmJsMain/kotlin/com/bumble/appyx/imageloader/ImageBitmap.wasmJs.kt b/demos/image-loader/common/src/wasmJsMain/kotlin/com/bumble/appyx/imageloader/ImageBitmap.wasmJs.kt
new file mode 100644
index 000000000..b614c08dd
--- /dev/null
+++ b/demos/image-loader/common/src/wasmJsMain/kotlin/com/bumble/appyx/imageloader/ImageBitmap.wasmJs.kt
@@ -0,0 +1,9 @@
+package com.bumble.appyx.imageloader
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toComposeImageBitmap
+
+actual fun ByteArray.toImageBitmap(): ImageBitmap {
+ // TODO This doesn't work. Fix this for web
+ return org.jetbrains.skia.Image.makeFromEncoded(this).toComposeImageBitmap()
+}
diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/build.gradle.kts b/demos/mkdocs/appyx-components/backstack/fader/web/build.gradle.kts
index 82e3a6a71..be29c162b 100644
--- a/demos/mkdocs/appyx-components/backstack/fader/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/backstack/fader/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-demos-backstack-fader-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -26,6 +42,11 @@ kotlin {
implementation(project(":demos:mkdocs:common"))
}
}
+ val wasmJsMain by getting {
+ dependencies {
+ implementation(project(":demos:mkdocs:common"))
+ }
+ }
}
}
@@ -36,4 +57,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/backstack/fader/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/fader/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/BackStackFaderSample.kt b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/BackStackFaderSample.kt
new file mode 100644
index 000000000..9dec56871
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/BackStackFaderSample.kt
@@ -0,0 +1,51 @@
+package com.bumble.appyx.demos.backstack.fader
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.backstack.BackStack
+import com.bumble.appyx.components.backstack.BackStackModel
+import com.bumble.appyx.components.backstack.operation.pop
+import com.bumble.appyx.components.backstack.operation.push
+import com.bumble.appyx.components.backstack.ui.fader.BackStackFader
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.ChildSize
+import com.bumble.appyx.demos.common.InteractionTarget
+import com.bumble.appyx.interactions.gesture.GestureFactory
+
+@Composable
+fun BackStackFaderSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ BackStackModel(
+ initialTarget = InteractionTarget.Element(),
+ savedStateMap = null
+ )
+ }
+ val backStack = BackStack(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { BackStackFader(it) },
+ gestureFactory = { GestureFactory.Noop() },
+ animationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2),
+ )
+ val actions = mapOf(
+ "Pop" to { backStack.pop() },
+ "Push" to { backStack.push(InteractionTarget.Element()) }
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = backStack,
+ actions = actions,
+ childSize = ChildSize.MAX,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/main.js.kt b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/main.js.kt
new file mode 100644
index 000000000..15e0c3fe8
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/fader/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.backstack.fader
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ BackStackFaderSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/fader/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/build.gradle.kts b/demos/mkdocs/appyx-components/backstack/parallax/web/build.gradle.kts
index 5f407941d..412125160 100644
--- a/demos/mkdocs/appyx-components/backstack/parallax/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/backstack/parallax/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-demos-backstack-parallax-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/backstack/parallax/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/parallax/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/BackStackParallaxSample.kt b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/BackStackParallaxSample.kt
new file mode 100644
index 000000000..16b53dc53
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/BackStackParallaxSample.kt
@@ -0,0 +1,51 @@
+package com.bumble.appyx.demos.backstack.parallax
+
+import androidx.compose.animation.core.Spring.StiffnessVeryLow
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.backstack.BackStack
+import com.bumble.appyx.components.backstack.BackStackModel
+import com.bumble.appyx.components.backstack.operation.pop
+import com.bumble.appyx.components.backstack.operation.push
+import com.bumble.appyx.components.backstack.ui.parallax.BackStackParallax
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.ChildSize
+import com.bumble.appyx.demos.common.InteractionTarget
+
+@Composable
+fun BackStackParallaxSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ BackStackModel(
+ initialTargets = List(5) { InteractionTarget.Element() },
+ savedStateMap = null,
+ )
+ }
+ val backStack =
+ BackStack(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { BackStackParallax(it) },
+ gestureFactory = { BackStackParallax.Gestures(it) },
+ animationSpec = spring(stiffness = StiffnessVeryLow),
+ )
+ val actions = mapOf(
+ "Pop" to { backStack.pop() },
+ "Push" to { backStack.push(InteractionTarget.Element()) }
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = backStack,
+ actions = actions,
+ childSize = ChildSize.MAX,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/main.js.kt b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/main.js.kt
new file mode 100644
index 000000000..810e353c1
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/parallax/main.js.kt
@@ -0,0 +1,44 @@
+package com.bumble.appyx.demos.backstack.parallax
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ BackStackParallaxSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(vertical = 16.dp)
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/parallax/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/build.gradle.kts b/demos/mkdocs/appyx-components/backstack/slider/web/build.gradle.kts
index d48bc8e78..c7f27e013 100644
--- a/demos/mkdocs/appyx-components/backstack/slider/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/backstack/slider/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-demos-backstack-slider-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/backstack/slider/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/slider/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/BackStackSliderSample.kt b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/BackStackSliderSample.kt
new file mode 100644
index 000000000..22c4b1571
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/BackStackSliderSample.kt
@@ -0,0 +1,52 @@
+package com.bumble.appyx.demos.backstack.slider
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.backstack.BackStack
+import com.bumble.appyx.components.backstack.BackStackModel
+import com.bumble.appyx.components.backstack.operation.pop
+import com.bumble.appyx.components.backstack.operation.push
+import com.bumble.appyx.components.backstack.ui.slider.BackStackSlider
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.ChildSize
+import com.bumble.appyx.demos.common.InteractionTarget
+import com.bumble.appyx.interactions.gesture.GestureFactory
+
+@Composable
+fun BackStackSliderSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ BackStackModel(
+ initialTarget = InteractionTarget.Element(),
+ savedStateMap = null
+ )
+ }
+ val backStack =
+ BackStack(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { BackStackSlider(it) },
+ gestureFactory = { GestureFactory.Noop() },
+ animationSpec = spring(stiffness = Spring.StiffnessVeryLow),
+ )
+ val actions = mapOf(
+ "Pop" to { backStack.pop() },
+ "Push" to { backStack.push(InteractionTarget.Element()) }
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = backStack,
+ actions = actions,
+ childSize = ChildSize.MAX,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/main.js.kt b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/main.js.kt
new file mode 100644
index 000000000..997896eca
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/slider/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.backstack.slider
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ BackStackSliderSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/slider/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/build.gradle.kts b/demos/mkdocs/appyx-components/backstack/stack3d/web/build.gradle.kts
index fc9946580..91685b189 100644
--- a/demos/mkdocs/appyx-components/backstack/stack3d/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-demos-backstack-stack3d-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/backstack/stack3d/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/BackStack3DSample.kt b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/BackStack3DSample.kt
new file mode 100644
index 000000000..9c31d253a
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/BackStack3DSample.kt
@@ -0,0 +1,51 @@
+package com.bumble.appyx.demos.backstack.stack3d
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.backstack.BackStack
+import com.bumble.appyx.components.backstack.BackStackModel
+import com.bumble.appyx.components.backstack.operation.pop
+import com.bumble.appyx.components.backstack.operation.push
+import com.bumble.appyx.components.backstack.ui.stack3d.BackStack3D
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.ChildSize
+import com.bumble.appyx.demos.common.InteractionTarget
+
+@Composable
+fun BackStack3DSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ BackStackModel(
+ initialTarget = InteractionTarget.Element(),
+ savedStateMap = null
+ )
+ }
+ val backStack =
+ BackStack(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { BackStack3D(it) },
+ gestureFactory = { BackStack3D.Gestures(it) },
+ animationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2),
+ )
+ val actions = mapOf(
+ "Pop" to { backStack.pop() },
+ "Push" to { backStack.push(InteractionTarget.Element()) }
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = backStack,
+ actions = actions,
+ childSize = ChildSize.MEDIUM,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/main.js.kt b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/main.js.kt
new file mode 100644
index 000000000..840b348dc
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/backstack/stack3d/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.backstack.stack3d
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ BackStack3DSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/backstack/stack3d/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/common/build.gradle.kts b/demos/mkdocs/appyx-components/common/build.gradle.kts
index 6c4418afe..944b9bde7 100644
--- a/demos/mkdocs/appyx-components/common/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "demos-mkdocs-appyx-components-common-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -29,4 +45,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/common/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/build.gradle.kts b/demos/mkdocs/appyx-components/experimental/datingcards/web/build.gradle.kts
index 3de9b88f3..b28e41caf 100644
--- a/demos/mkdocs/appyx-components/experimental/datingcards/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-demos-experimental-datingcards-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/experimental/datingcards/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/DatingCardsSample.kt b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/DatingCardsSample.kt
new file mode 100644
index 000000000..a2532ecb5
--- /dev/null
+++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/DatingCardsSample.kt
@@ -0,0 +1,49 @@
+package com.bumble.appyx.demos.experimental.datingcards
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.experimental.cards.Cards
+import com.bumble.appyx.components.experimental.cards.CardsModel
+import com.bumble.appyx.components.experimental.cards.operation.like
+import com.bumble.appyx.components.experimental.cards.operation.pass
+import com.bumble.appyx.components.experimental.cards.ui.CardsVisualisation
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.ChildSize
+import com.bumble.appyx.demos.common.InteractionTarget
+
+@Composable
+fun DatingCardsSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val cards = remember {
+ Cards(
+ model = CardsModel(
+ initialItems = List(15) { InteractionTarget.Element(it) },
+ savedStateMap = null
+ ),
+ visualisation = { CardsVisualisation(it) },
+ gestureFactory = { CardsVisualisation.Gestures(it) },
+ animateSettle = true
+ )
+ }
+
+ val animationSpec: AnimationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2)
+ val actions = mapOf(
+ "Pass" to { cards.pass(animationSpec = animationSpec) },
+ "Like" to { cards.like(animationSpec = animationSpec) },
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = cards,
+ actions = actions,
+ childSize = ChildSize.MAX,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/main.js.kt b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/main.js.kt
new file mode 100644
index 000000000..41c268dee
--- /dev/null
+++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/datingcards/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.experimental.datingcards
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ DatingCardsSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/experimental/datingcards/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/build.gradle.kts b/demos/mkdocs/appyx-components/experimental/puzzle15/web/build.gradle.kts
index 40d079ae9..8f6739d1f 100644
--- a/demos/mkdocs/appyx-components/experimental/puzzle15/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-demos-experimental-puzzle15-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/puzzle15/main.js.kt b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/puzzle15/main.js.kt
new file mode 100644
index 000000000..ab71d5098
--- /dev/null
+++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/experimental/puzzle15/main.js.kt
@@ -0,0 +1,50 @@
+package com.bumble.appyx.demos.experimental.puzzle15
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.components.experimental.puzzle15.ui.Puzzle15Ui
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+import com.bumble.appyx.demos.common.color_primary
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ Puzzle15Ui(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ accentColor = color_primary,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/experimental/puzzle15/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/fader/web/build.gradle.kts
index 89894921e..5b40e1c9a 100644
--- a/demos/mkdocs/appyx-components/spotlight/fader/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/spotlight/fader/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-components-spotlight-fader-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/fader/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/fader/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/SpotlightFaderSample.kt b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/SpotlightFaderSample.kt
new file mode 100644
index 000000000..1f1866f45
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/SpotlightFaderSample.kt
@@ -0,0 +1,65 @@
+package com.bumble.appyx.demos.spotlight.fader
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.spotlight.Spotlight
+import com.bumble.appyx.components.spotlight.SpotlightModel
+import com.bumble.appyx.components.spotlight.operation.next
+import com.bumble.appyx.components.spotlight.operation.previous
+import com.bumble.appyx.components.spotlight.ui.fader.SpotlightFader
+import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.ChildSize
+import com.bumble.appyx.demos.common.InteractionTarget
+import com.bumble.appyx.interactions.model.transition.Operation
+
+@Composable
+fun SpotlightFaderSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ SpotlightModel(
+ items = List(7) { InteractionTarget.Element(it) },
+ initialActiveIndex = 0f,
+ savedStateMap = null
+ )
+ }
+ val spotlight =
+ Spotlight(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { SpotlightFader(it) },
+ gestureFactory = { SpotlightSlider.Gestures(it) },
+ )
+ val animationSpec: AnimationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2)
+ val actions = mapOf(
+ "Prev" to {
+ spotlight.previous(
+ mode = Operation.Mode.KEYFRAME,
+ animationSpec = animationSpec,
+ )
+ },
+ "Next" to {
+ spotlight.next(
+ mode = Operation.Mode.KEYFRAME,
+ animationSpec = animationSpec,
+ )
+ },
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = spotlight,
+ actions = actions,
+ childSize = ChildSize.MAX,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/main.js.kt b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/main.js.kt
new file mode 100644
index 000000000..2c7277c71
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/fader/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.spotlight.fader
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ SpotlightFaderSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 60.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/fader/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/slider/web/build.gradle.kts
index 0626d9ca4..48fd78ee9 100644
--- a/demos/mkdocs/appyx-components/spotlight/slider/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/spotlight/slider/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-components-spotlight-slider-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/slider/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/slider/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/SpotlightSliderSample.kt b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/SpotlightSliderSample.kt
new file mode 100644
index 000000000..2ad4a672b
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/SpotlightSliderSample.kt
@@ -0,0 +1,51 @@
+package com.bumble.appyx.demos.spotlight.slider
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.spotlight.Spotlight
+import com.bumble.appyx.components.spotlight.SpotlightModel
+import com.bumble.appyx.components.spotlight.operation.first
+import com.bumble.appyx.components.spotlight.operation.last
+import com.bumble.appyx.components.spotlight.operation.next
+import com.bumble.appyx.components.spotlight.operation.previous
+import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.InteractionTarget
+
+@Composable
+fun SpotlightSliderSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ SpotlightModel(
+ items = List(7) { InteractionTarget.Element(it) },
+ initialActiveIndex = 1f,
+ savedStateMap = null
+ )
+ }
+ val spotlight =
+ Spotlight(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { SpotlightSlider(it, model.currentState) },
+ gestureFactory = { SpotlightSlider.Gestures(it) }
+ )
+ val actions = mapOf(
+ "First" to { spotlight.first() },
+ "Prev" to { spotlight.previous() },
+ "Next" to { spotlight.next() },
+ "Last" to { spotlight.last() },
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = spotlight,
+ actions = actions,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/main.js.kt b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/main.js.kt
new file mode 100644
index 000000000..eb8f4b292
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/slider/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.spotlight.slider
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ SpotlightSliderSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 60.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/slider/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/build.gradle.kts
index b08345ee8..975eab2dd 100644
--- a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-components-spotlight-slider-rotation-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/SpotlightSliderRotationSample.kt b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/SpotlightSliderRotationSample.kt
new file mode 100644
index 000000000..2fcb17043
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/SpotlightSliderRotationSample.kt
@@ -0,0 +1,53 @@
+package com.bumble.appyx.demos.spotlight.sliderrotation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.spotlight.Spotlight
+import com.bumble.appyx.components.spotlight.SpotlightModel
+import com.bumble.appyx.components.spotlight.operation.first
+import com.bumble.appyx.components.spotlight.operation.last
+import com.bumble.appyx.components.spotlight.operation.next
+import com.bumble.appyx.components.spotlight.operation.previous
+import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider
+import com.bumble.appyx.components.spotlight.ui.sliderrotation.SpotlightSliderRotation
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.InteractionTarget
+
+@Composable
+fun SpotlightSliderRotationSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ SpotlightModel(
+ items = List(7) { InteractionTarget.Element(it) },
+ initialActiveIndex = 1f,
+ savedStateMap = null
+ )
+ }
+ val spotlight =
+ Spotlight(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { SpotlightSliderRotation(it, model.currentState) },
+ gestureFactory = { SpotlightSlider.Gestures(it) }
+ )
+ val actions = mapOf(
+ "First" to { spotlight.first() },
+ "Prev" to { spotlight.previous() },
+ "Next" to { spotlight.next() },
+ "Last" to { spotlight.last() },
+
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = spotlight,
+ actions = actions,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/main.js.kt b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/main.js.kt
new file mode 100644
index 000000000..6999e1c8d
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderrotation/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.spotlight.sliderrotation
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ SpotlightSliderRotationSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 85.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderrotation/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/build.gradle.kts
index c58816d23..5269df2ab 100644
--- a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-components-spotlight-slider-scale-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/SpotlightSliderScaleSample.kt b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/SpotlightSliderScaleSample.kt
new file mode 100644
index 000000000..8c1e8baba
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/SpotlightSliderScaleSample.kt
@@ -0,0 +1,52 @@
+package com.bumble.appyx.demos.spotlight.sliderscale
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.spotlight.Spotlight
+import com.bumble.appyx.components.spotlight.SpotlightModel
+import com.bumble.appyx.components.spotlight.operation.first
+import com.bumble.appyx.components.spotlight.operation.last
+import com.bumble.appyx.components.spotlight.operation.next
+import com.bumble.appyx.components.spotlight.operation.previous
+import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider
+import com.bumble.appyx.components.spotlight.ui.sliderscale.SpotlightSliderScale
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.InteractionTarget
+
+@Composable
+fun SpotlightSliderScaleSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ SpotlightModel(
+ items = List(7) { InteractionTarget.Element(it) },
+ initialActiveIndex = 1f,
+ savedStateMap = null
+ )
+ }
+ val spotlight =
+ Spotlight(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { SpotlightSliderScale(it, model.currentState) },
+ gestureFactory = { SpotlightSlider.Gestures(it) }
+ )
+ val actions = mapOf(
+ "First" to { spotlight.first() },
+ "Prev" to { spotlight.previous() },
+ "Next" to { spotlight.next() },
+ "Last" to { spotlight.last() },
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = spotlight,
+ actions = actions,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/main.js.kt b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/main.js.kt
new file mode 100644
index 000000000..72ed61307
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/sliderscale/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.spotlight.sliderscale
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ SpotlightSliderScaleSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 60.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/sliderscale/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/build.gradle.kts b/demos/mkdocs/appyx-components/spotlight/stack3d/web/build.gradle.kts
index 27eaa27ad..7e19b4aae 100644
--- a/demos/mkdocs/appyx-components/spotlight/stack3d/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-components-spotlight-stack3d-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-components/spotlight/stack3d/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/SpotlightStack3DSample.kt b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/SpotlightStack3DSample.kt
new file mode 100644
index 000000000..12279a8ce
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/SpotlightStack3DSample.kt
@@ -0,0 +1,72 @@
+package com.bumble.appyx.demos.spotlight.stack3d
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.components.spotlight.Spotlight
+import com.bumble.appyx.components.spotlight.SpotlightModel
+import com.bumble.appyx.components.spotlight.operation.next
+import com.bumble.appyx.components.spotlight.operation.previous
+import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider
+import com.bumble.appyx.components.spotlight.ui.stack3d.SpotlightStack3D
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.ChildSize
+import com.bumble.appyx.demos.common.InteractionTarget
+import com.bumble.appyx.interactions.model.transition.Operation
+
+@Composable
+fun SpotlightStack3DSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ SpotlightModel(
+ items = List(7) { InteractionTarget.Element(it) },
+ initialActiveIndex = 0f,
+ savedStateMap = null
+ )
+ }
+ val spotlight =
+ Spotlight(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { SpotlightStack3D(it, model.currentState) },
+ gestureFactory = {
+ SpotlightSlider.Gestures(
+ transitionBounds = it,
+ orientation = Orientation.Vertical,
+ reverseOrientation = true,
+ )
+ },
+ )
+ val animationSpec: AnimationSpec = spring(stiffness = Spring.StiffnessVeryLow * 2)
+ val actions = mapOf(
+ "Prev" to {
+ spotlight.previous(
+ mode = Operation.Mode.KEYFRAME,
+ animationSpec = animationSpec,
+ )
+ },
+ "Next" to {
+ spotlight.next(
+ mode = Operation.Mode.KEYFRAME,
+ animationSpec = animationSpec,
+ )
+ },
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = spotlight,
+ actions = actions,
+ childSize = ChildSize.MEDIUM,
+ modifier = modifier,
+ )
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/main.js.kt b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/main.js.kt
new file mode 100644
index 000000000..39f40cc69
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/spotlight/stack3d/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.spotlight.stack3d
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+import com.bumble.appyx.demos.common.color_dark
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ SpotlightStack3DSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 60.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-components/spotlight/stack3d/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/build.gradle.kts
index c8c76e0b4..cefebc6a7 100644
--- a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-interactions-gestures-dragpredication-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPrediction.kt b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPrediction.kt
new file mode 100644
index 000000000..563c27997
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPrediction.kt
@@ -0,0 +1,230 @@
+@file:Suppress("MatchingDeclarationName")
+
+package com.bumble.appyx.demos.dragprediction
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+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.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.LinearGradientShader
+import androidx.compose.ui.graphics.ShaderBrush
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
+import com.bumble.appyx.components.internal.testdrive.TestDrive
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.demos.dragprediction.DragPredictionVisualisation.Companion.toTargetUiState
+import com.bumble.appyx.demos.dragprediction.InteractionTarget.Child1
+import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer
+import com.bumble.appyx.interactions.model.transition.Keyframes
+import com.bumble.appyx.interactions.model.transition.Update
+import com.bumble.appyx.interactions.gesture.GestureSettleConfig
+import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup
+
+enum class InteractionTarget {
+ Child1
+}
+
+@Composable
+fun DragPrediction(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember { TestDriveModel(Child1, null) }
+ val testDrive = remember {
+ TestDrive(
+ scope = coroutineScope,
+ model = model,
+ progressAnimationSpec = spring(visibilityThreshold = 0.001f),
+ visualisation = { DragPredictionVisualisation(it) },
+ gestureFactory = { DragPredictionVisualisation.Gestures(it) },
+ gestureSettleConfig = GestureSettleConfig(0.25f)
+ )
+ }
+
+ AppyxComponentSetup(testDrive)
+
+ val output = model.output.collectAsState().value
+ val currentTarget: androidx.compose.runtime.State?> =
+ when (output) {
+ is Keyframes -> output.currentSegmentTargetStateFlow.collectAsState(null)
+ is Update -> remember(output) { mutableStateOf(output.currentTargetState) }
+ }
+
+ @Suppress("UnusedPrivateMember")
+ val index = when (output) {
+ is Keyframes -> output.currentIndex
+ is Update -> null
+ }
+
+ Box(
+ modifier = modifier,
+ ) {
+ Background(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ currentTarget = currentTarget.value
+ )
+ Box(
+ modifier = Modifier.padding(24.dp, 24.dp)
+ ) {
+ Target(elementState = B, alpha = 0.15f)
+ Target(elementState = C, alpha = 0.15f)
+ Target(elementState = D, alpha = 0.15f)
+ Target(elementState = currentTarget.value?.elementState, alpha = 0.65f)
+ ModelUi(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ testDrive = testDrive,
+ model = model
+ )
+ Controls(
+ testDrive = testDrive
+ )
+ }
+ }
+}
+
+@Composable
+fun Background(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ currentTarget: TestDriveModel.State?,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ val backgroundColor1 = animateColorAsState(
+ when (currentTarget?.elementState) {
+ A -> color_neutral1
+ B -> color_neutral2
+ C -> color_neutral3
+ D -> color_neutral4
+ null -> color_bright
+ },
+ animationSpec = spring(stiffness = Spring.StiffnessVeryLow)
+ )
+
+ Box(
+ modifier
+ .zIndex(0f)
+ .background(
+ ShaderBrush(
+ LinearGradientShader(
+ from = Offset.Zero,
+ to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()),
+ colors = listOf(color_bright, backgroundColor1.value)
+ )
+ )
+ )
+ )
+}
+
+@Composable
+fun Target(
+ elementState: ElementState?,
+ alpha: Float,
+ modifier: Modifier = Modifier,
+) {
+ val targetUiState = elementState?.toTargetUiState()
+ targetUiState?.let {
+ Box(
+ modifier = modifier
+ .size(60.dp)
+ .offset(
+ targetUiState.positionOffset.value.offset.x,
+ targetUiState.positionOffset.value.offset.y
+ )
+ .scale(targetUiState.scale.value)
+ .rotate(targetUiState.rotationZ.value)
+ .alpha(alpha)
+ .background(
+ color = targetUiState.backgroundColor.value,
+ shape = RoundedCornerShape(targetUiState.roundedCorners.value)
+ )
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = elementState.name,
+ fontSize = 24.sp,
+ color = Color.White
+ )
+ }
+ }
+}
+
+@Composable
+fun ModelUi(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ testDrive: TestDrive,
+ model: TestDriveModel,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ AppyxInteractionsContainer(
+ appyxComponent = testDrive,
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ modifier = modifier
+ ) {
+ Box(
+ modifier = Modifier.size(60.dp)
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = model.output.collectAsState().value.currentTargetState.elementState.name,
+ fontSize = 24.sp,
+ color = Color.White
+ )
+ }
+ }
+}
+
+@Suppress("UnusedPrivateMember")
+@Composable
+private fun Controls(
+ testDrive: TestDrive,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ Column(
+ modifier = modifier.zIndex(1f),
+ verticalArrangement = Arrangement.Bottom,
+ horizontalAlignment = CenterHorizontally
+ ) {
+ Row {
+ Spacer(Modifier.width(16.dp))
+ }
+
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPredictionVisualisation.kt b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPredictionVisualisation.kt
new file mode 100644
index 000000000..66caff8c7
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/DragPredictionVisualisation.kt
@@ -0,0 +1,136 @@
+package com.bumble.appyx.demos.dragprediction
+
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.components.internal.testdrive.operation.MoveTo
+import com.bumble.appyx.interactions.ui.context.TransitionBounds
+import com.bumble.appyx.interactions.ui.context.UiContext
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT
+import com.bumble.appyx.interactions.gesture.Gesture
+import com.bumble.appyx.interactions.gesture.GestureFactory
+import com.bumble.appyx.interactions.gesture.dragDirection8
+import com.bumble.appyx.interactions.ui.DefaultAnimationSpec
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.RotationZ
+import com.bumble.appyx.interactions.ui.property.impl.Scale
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState
+import com.bumble.appyx.transitionmodel.BaseVisualisation
+import com.bumble.appyx.utils.multiplatform.AppyxLogger
+
+class DragPredictionVisualisation(
+ uiContext: UiContext,
+ uiAnimationSpec: SpringSpec = DefaultAnimationSpec
+) : BaseVisualisation, TargetUiState, MutableUiState>(
+ uiContext = uiContext,
+ defaultAnimationSpec = uiAnimationSpec,
+) {
+ override fun TestDriveModel.State.toUiTargets():
+ List> =
+ listOf(
+ MatchedTargetUiState(element, elementState.toTargetUiState()).also {
+ AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}")
+ }
+ )
+
+ companion object {
+ fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState =
+ when (this) {
+ A -> topLeftCorner
+ B -> topRightCorner
+ C -> BottomRightCorner
+ D -> bottomLeftCorner
+ }
+
+ private val topLeftCorner = TargetUiState(
+ positionOffset = PositionOffset.Target(DpOffset(0.dp, 0.dp)),
+ scale = Scale.Target(1f),
+ backgroundColor = BackgroundColor.Target(color_primary)
+ )
+
+ private val topRightCorner = TargetUiState(
+ positionOffset = PositionOffset.Target(DpOffset(180.dp, 30.dp)),
+ scale = Scale.Target(2f, TransformOrigin(0f, 0f)),
+ backgroundColor = BackgroundColor.Target(color_dark)
+ )
+
+ private val BottomRightCorner = TargetUiState(
+ positionOffset = PositionOffset.Target(DpOffset(180.dp, 180.dp)),
+ scale = Scale.Target(2f, TransformOrigin(0f, 0f)),
+ rotationZ = RotationZ.Target(90f),
+ backgroundColor = BackgroundColor.Target(color_secondary)
+ )
+
+ private val bottomLeftCorner = TargetUiState(
+ positionOffset = PositionOffset.Target(DpOffset(30.dp, 180.dp)),
+ scale = Scale.Target(2f, TransformOrigin(0f, 0f)),
+ rotationZ = RotationZ.Target(180f),
+ backgroundColor = BackgroundColor.Target(color_tertiary)
+ )
+ }
+
+ override fun mutableUiStateFor(
+ uiContext: UiContext,
+ targetUiState: TargetUiState
+ ): MutableUiState =
+ targetUiState.toMutableUiState(uiContext)
+
+
+ @Suppress("UnusedPrivateMember")
+ class Gestures(
+ transitionBounds: TransitionBounds,
+ ) : GestureFactory> {
+ private val maxX = topRightCorner.positionOffset.value.offset.x - topLeftCorner.positionOffset.value.offset.x
+ private val maxY = bottomLeftCorner.positionOffset.value.offset.y - topLeftCorner.positionOffset.value.offset.y
+
+ @Suppress("ComplexMethod")
+ override fun createGesture(
+ state: TestDriveModel.State,
+ delta: Offset,
+ density: Density
+ ): Gesture> {
+ val maxX = with(density) { maxX.toPx() }
+ val maxY = with(density) { maxY.toPx() }
+
+ val direction = dragDirection8(delta)
+ return when (state.elementState) {
+ A -> when (direction) {
+ RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f))
+ DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY))
+ DOWN -> Gesture(MoveTo(D), Offset(0f, maxY))
+ else -> Gesture.Noop()
+ }
+
+ B -> when (direction) {
+ LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f))
+ else -> Gesture.Noop()
+ }
+
+ C -> when (direction) {
+ UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY))
+ else -> Gesture.Noop()
+ }
+
+ D -> when (direction) {
+ UP -> Gesture(MoveTo(A), Offset(0f, -maxY))
+ else -> Gesture.Noop()
+ }
+ }
+ }
+ }
+}
+
diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/TargetUiState.kt b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/TargetUiState.kt
new file mode 100644
index 000000000..e79f03f3a
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/TargetUiState.kt
@@ -0,0 +1,18 @@
+package com.bumble.appyx.demos.dragprediction
+
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.RotationZ
+import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners
+import com.bumble.appyx.interactions.ui.property.impl.Scale
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs
+
+@Suppress("unused")
+@MutableUiStateSpecs
+class TargetUiState(
+ val positionOffset: PositionOffset.Target,
+ val scale: Scale.Target,
+ val rotationZ: RotationZ.Target = RotationZ.Target(0f),
+ val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10),
+ val backgroundColor: BackgroundColor.Target,
+)
diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/main.js.kt b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/main.js.kt
new file mode 100644
index 000000000..8cb6ad058
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/dragprediction/main.js.kt
@@ -0,0 +1,57 @@
+package com.bumble.appyx.demos.dragprediction
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+
+val color_bright = Color(0xFFFFFFFF)
+val color_dark = Color(0xFF353535)
+val color_primary = Color(0xFFFFC629)
+val color_secondary = Color(0xFFFE9763)
+val color_tertiary = Color(0xFF855353)
+val color_neutral1 = Color(0xFFD2D7DF)
+val color_neutral2 = Color(0xFF8A897C)
+val color_neutral3 = Color(0xFFD9E8ED)
+val color_neutral4 = Color(0xFFBEA489)
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ DragPrediction(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/dragprediction/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/build.gradle.kts
index 984bcaf61..d6c84aeda 100644
--- a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-interactions-gestures-incompletedrag-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -35,4 +51,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDrag.kt b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDrag.kt
new file mode 100644
index 000000000..064e9b164
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDrag.kt
@@ -0,0 +1,194 @@
+@file:Suppress("MatchingDeclarationName")
+
+package com.bumble.appyx.demos.incompletedrag
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+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.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.LinearGradientShader
+import androidx.compose.ui.graphics.ShaderBrush
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
+import com.bumble.appyx.components.internal.testdrive.TestDrive
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.components.internal.testdrive.operation.next
+import com.bumble.appyx.demos.incompletedrag.InteractionTarget.Child1
+import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer
+import com.bumble.appyx.interactions.gesture.GestureReferencePoint
+import com.bumble.appyx.interactions.model.transition.Operation.Mode.IMMEDIATE
+import com.bumble.appyx.interactions.gesture.GestureSettleConfig
+import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup
+
+enum class InteractionTarget {
+ Child1
+}
+
+@Composable
+fun IncompleteDrag(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember { TestDriveModel(Child1, null) }
+ val testDrive = remember {
+ TestDrive(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { IncompleteDragVisualisation(it) },
+ gestureFactory = { IncompleteDragVisualisation.Gestures(it) },
+ gestureSettleConfig = GestureSettleConfig(0.15f)
+ )
+ }
+
+ AppyxComponentSetup(testDrive)
+
+ Box(
+ modifier = modifier,
+ ) {
+ Background(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ model = model
+ )
+ Box(
+ modifier = Modifier.padding(24.dp, 24.dp)
+ ) {
+ ModelUi(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ testDrive = testDrive,
+ model = model
+ )
+ Controls(
+ testDrive = testDrive
+ )
+ }
+ }
+}
+
+@Composable
+fun Background(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ model: TestDriveModel,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ val output = model.output.collectAsState()
+ val currentTarget = output.value.currentTargetState.elementState
+ val backgroundColor1 = animateColorAsState(
+ when (currentTarget) {
+ A -> color_neutral1
+ B -> color_neutral2
+ C -> color_neutral3
+ D -> color_neutral4
+ }
+ )
+
+ Box(
+ modifier
+ .zIndex(0f)
+ .background(
+ ShaderBrush(
+ LinearGradientShader(
+ from = Offset.Zero,
+ to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()),
+ colors = listOf(color_bright, backgroundColor1.value)
+ )
+ )
+ )
+ )
+}
+
+@Composable
+fun ModelUi(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ testDrive: TestDrive,
+ model: TestDriveModel,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ val output = model.output.collectAsState()
+ val currentTarget = output.value.currentTargetState.elementState
+ val backgroundColor1 = animateColorAsState(
+ when (currentTarget) {
+ A -> color_neutral1
+ B -> color_neutral2
+ C -> color_neutral3
+ D -> color_neutral4
+ }
+ )
+
+ AppyxInteractionsContainer(
+ appyxComponent = testDrive,
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ gestureRelativeTo = GestureReferencePoint.Element,
+ modifier = modifier
+ )
+ {
+ Box(
+ modifier = Modifier
+ .size(60.dp)
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = model.output.collectAsState().value.currentTargetState.elementState.name,
+ fontSize = 24.sp,
+ color = Color.White
+ )
+ }
+ }
+}
+
+@Composable
+private fun Controls(
+ testDrive: TestDrive,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ Column(
+ modifier = modifier.zIndex(1f),
+ verticalArrangement = Arrangement.Bottom,
+ horizontalAlignment = CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .background(color_primary, shape = RoundedCornerShape(4.dp))
+ .clickable {
+ testDrive.next(
+ mode = IMMEDIATE, animationSpec = spring(
+ stiffness = Spring.StiffnessLow,
+ dampingRatio = Spring.DampingRatioLowBouncy,
+ )
+ )
+ }
+ .padding(horizontal = 18.dp, vertical = 9.dp)
+ ) {
+ Text("Next")
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDragVisualisation.kt b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDragVisualisation.kt
new file mode 100644
index 000000000..0716497d1
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/IncompleteDragVisualisation.kt
@@ -0,0 +1,154 @@
+package com.bumble.appyx.demos.incompletedrag
+
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.components.internal.testdrive.operation.MoveTo
+import com.bumble.appyx.interactions.ui.context.TransitionBounds
+import com.bumble.appyx.interactions.ui.context.UiContext
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNLEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPRIGHT
+import com.bumble.appyx.interactions.gesture.Gesture
+import com.bumble.appyx.interactions.gesture.GestureFactory
+import com.bumble.appyx.interactions.gesture.dragDirection8
+import com.bumble.appyx.interactions.ui.DefaultAnimationSpec
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.RotationZ
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomEnd
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomStart
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopEnd
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopStart
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState
+import com.bumble.appyx.transitionmodel.BaseVisualisation
+import com.bumble.appyx.utils.multiplatform.AppyxLogger
+
+class IncompleteDragVisualisation(
+ uiContext: UiContext,
+ uiAnimationSpec: SpringSpec = DefaultAnimationSpec
+) : BaseVisualisation, TargetUiState, MutableUiState>(
+ uiContext = uiContext,
+ defaultAnimationSpec = uiAnimationSpec,
+) {
+ override fun TestDriveModel.State.toUiTargets():
+ List> =
+ listOf(
+ MatchedTargetUiState(element, elementState.toTargetUiState()).also {
+ AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}")
+ }
+ )
+
+ companion object {
+ val bottomOffset = DpOffset(0.dp, (-50).dp)
+
+ fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState =
+ when (this) {
+ A -> topLeftCorner
+ B -> topRightCorner
+ C -> bottomRightCorner
+ D -> bottomLeftCorner
+ }
+
+ private val topLeftCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(TopStart),
+ positionOffset = PositionOffset.Target(DpOffset.Zero),
+ rotationZ = RotationZ.Target(0f),
+ backgroundColor = BackgroundColor.Target(color_primary)
+ )
+
+ private val topRightCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(TopEnd),
+ positionOffset = PositionOffset.Target(DpOffset.Zero),
+ rotationZ = RotationZ.Target(180f),
+ backgroundColor = BackgroundColor.Target(color_dark)
+ )
+
+ private val bottomRightCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(BottomEnd),
+ positionOffset = PositionOffset.Target(bottomOffset),
+ rotationZ = RotationZ.Target(270f),
+ backgroundColor = BackgroundColor.Target(color_secondary)
+ )
+
+ private val bottomLeftCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(BottomStart),
+ positionOffset = PositionOffset.Target(bottomOffset),
+ rotationZ = RotationZ.Target(540f),
+ backgroundColor = BackgroundColor.Target(color_tertiary)
+ )
+ }
+
+ override fun mutableUiStateFor(
+ uiContext: UiContext,
+ targetUiState: TargetUiState
+ ): MutableUiState =
+ targetUiState.toMutableUiState(uiContext)
+
+
+ class Gestures(
+ private val transitionBounds: TransitionBounds,
+ ) : GestureFactory> {
+
+ @Suppress("ComplexMethod")
+ override fun createGesture(
+ state: TestDriveModel.State,
+ delta: Offset,
+ density: Density
+ ): Gesture> {
+ // FIXME 60.dp is the assumed element size, connect it to real value
+ // TODO automate this whole calculation based on .onPlaced centers of targetUiStates
+ val maxX = with(density) {
+ (transitionBounds.widthDp - 60.dp).toPx()
+ }
+ val maxY = with(density) {
+ (transitionBounds.heightDp + bottomOffset.y - 60.dp).toPx()
+ }
+
+ val direction = dragDirection8(delta)
+ return when (state.elementState) {
+ A -> when (direction) {
+ RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f))
+ DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY))
+ DOWN -> Gesture(MoveTo(D), Offset(0f, maxY))
+ else -> Gesture.Noop()
+ }
+
+ B -> when (direction) {
+ DOWN -> Gesture(MoveTo(C), Offset(0f, maxY))
+ DOWNLEFT -> Gesture(MoveTo(D), Offset(-maxX, maxY))
+ LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f))
+ else -> Gesture.Noop()
+ }
+
+ C -> when (direction) {
+ LEFT -> Gesture(MoveTo(D), Offset(-maxX, 0f))
+ UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY))
+ UP -> Gesture(MoveTo(B), Offset(0f, -maxY))
+ else -> Gesture.Noop()
+ }
+
+ D -> when (direction) {
+ UP -> Gesture(MoveTo(A), Offset(0f, -maxY))
+ UPRIGHT -> Gesture(MoveTo(B), Offset(maxX, -maxY))
+ RIGHT -> Gesture(MoveTo(C), Offset(maxX, 0f))
+ else -> Gesture.Noop()
+ }
+ }
+ }
+ }
+}
+
diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/TargetUiState.kt b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/TargetUiState.kt
new file mode 100644
index 000000000..421759948
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/TargetUiState.kt
@@ -0,0 +1,18 @@
+package com.bumble.appyx.demos.incompletedrag
+
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.RotationZ
+import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs
+
+@Suppress("unused")
+@MutableUiStateSpecs
+class TargetUiState(
+ val positionAlignment: PositionAlignment.Target,
+ val positionOffset: PositionOffset.Target,
+ val rotationZ: RotationZ.Target,
+ val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10),
+ val backgroundColor: BackgroundColor.Target,
+)
diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/main.js.kt b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/main.js.kt
new file mode 100644
index 000000000..421193d13
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/incompletedrag/main.js.kt
@@ -0,0 +1,57 @@
+package com.bumble.appyx.demos.incompletedrag
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+
+val color_bright = Color(0xFFFFFFFF)
+val color_dark = Color(0xFF353535)
+val color_primary = Color(0xFFFFC629)
+val color_secondary = Color(0xFFFE9763)
+val color_tertiary = Color(0xFF855353)
+val color_neutral1 = Color(0xFFD2D7DF)
+val color_neutral2 = Color(0xFF8A897C)
+val color_neutral3 = Color(0xFFD9E8ED)
+val color_neutral4 = Color(0xFFBEA489)
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ IncompleteDrag(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..cb31659d7
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/gestures/incompletedrag/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/interactions/observemp/web/build.gradle.kts
index b34f2c892..1f3d46046 100644
--- a/demos/mkdocs/appyx-interactions/interactions/observemp/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-interactions-observemp-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -31,4 +47,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/interactions/observemp/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/ObserveMotionPropertiesSample.kt b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/ObserveMotionPropertiesSample.kt
new file mode 100644
index 000000000..3223c42a8
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/ObserveMotionPropertiesSample.kt
@@ -0,0 +1,137 @@
+package com.bumble.appyx.demos.observemp
+
+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.fillMaxSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.bumble.appyx.components.spotlight.Spotlight
+import com.bumble.appyx.components.spotlight.SpotlightModel
+import com.bumble.appyx.components.spotlight.operation.first
+import com.bumble.appyx.components.spotlight.operation.last
+import com.bumble.appyx.components.spotlight.operation.next
+import com.bumble.appyx.components.spotlight.operation.previous
+import com.bumble.appyx.components.spotlight.ui.slider.SpotlightSlider
+import com.bumble.appyx.components.spotlight.ui.sliderrotation.SpotlightSliderRotation
+import com.bumble.appyx.demos.common.AppyxWebSample
+import com.bumble.appyx.demos.common.InteractionTarget
+import com.bumble.appyx.demos.common.colors
+import com.bumble.appyx.interactions.model.Element
+import com.bumble.appyx.interactions.ui.property.impl.RotationY
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment
+import com.bumble.appyx.interactions.ui.property.motionPropertyRenderValue
+import kotlin.math.roundToInt
+
+@Composable
+fun ObserveMotionPropertiesSample(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember {
+ SpotlightModel(
+ items = List(7) { InteractionTarget.Element(it) },
+ initialActiveIndex = 1f,
+ savedStateMap = null
+ )
+ }
+ val spotlight = remember {
+ Spotlight(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { SpotlightSliderRotation(it, model.currentState) },
+ gestureFactory = { SpotlightSlider.Gestures(it) }
+ )
+ }
+ val actions = mapOf(
+ "First" to { spotlight.first() },
+ "Prev" to { spotlight.previous() },
+ "Next" to { spotlight.next() },
+ "Last" to { spotlight.last() },
+
+ )
+ AppyxWebSample(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ appyxComponent = spotlight,
+ actions = actions,
+ modifier = modifier,
+ ) {
+ ModalUi(it, false)
+ }
+}
+
+@Composable
+fun ModalUi(
+ element: Element,
+ isChildMaxSize: Boolean,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .fillMaxSize(if (isChildMaxSize) 1f else 0.9f)
+ .background(
+ color = when (val target = element.interactionTarget) {
+ is com.bumble.appyx.demos.common.InteractionTarget.Element -> colors.getOrElse(
+ target.idx % colors.size
+ ) { Color.Cyan }
+
+ else -> {
+ Color.Cyan
+ }
+ },
+ shape = RoundedCornerShape(if (isChildMaxSize) 0 else 8)
+ )
+ ) {
+ Column(
+ modifier = Modifier.align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = element.interactionTarget.toString(),
+ fontSize = 12.sp,
+ color = Color.White
+ )
+ val alignment =
+ motionPropertyRenderValue()
+ if (alignment != null) {
+ val offsetPercentage = roundFloatToTwoDecimals(
+ alignment.outsideAlignment.horizontalBias * 100
+ )
+
+ Text(
+ text = "Offset:\n$offsetPercentage%",
+ fontSize = 12.sp,
+ textAlign = TextAlign.Center,
+ color = Color.White
+ )
+ }
+ val rotationY = motionPropertyRenderValue()
+ if (rotationY != null) {
+ Text(
+ text = "Rotation:\n${roundFloatToTwoDecimals(rotationY)}°",
+ fontSize = 12.sp,
+ textAlign = TextAlign.Center,
+ color = Color.White
+ )
+ }
+ }
+ }
+}
+
+private fun roundFloatToTwoDecimals(float: Float): Double {
+ return (float * 100.0).roundToInt() / 100.0
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/main.js.kt b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/main.js.kt
new file mode 100644
index 000000000..be0f3f002
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/observemp/main.js.kt
@@ -0,0 +1,47 @@
+package com.bumble.appyx.demos.observemp
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.common.color_dark
+
+external fun onWasmReady(onReady: () -> Unit)
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ onWasmReady {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ ObserveMotionPropertiesSample(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 85.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..5ca3e007b
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/observemp/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/interactions/sample1/web/build.gradle.kts
index ee1fbb5ef..94023af25 100644
--- a/demos/mkdocs/appyx-interactions/interactions/sample1/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-interactions-sample1-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/interactions/sample1/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1.kt b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1.kt
new file mode 100644
index 000000000..b6db13809
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1.kt
@@ -0,0 +1,189 @@
+@file:Suppress("MatchingDeclarationName")
+package com.bumble.appyx.demos.sample1
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+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.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.LinearGradientShader
+import androidx.compose.ui.graphics.ShaderBrush
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
+import com.bumble.appyx.components.internal.testdrive.TestDrive
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.components.internal.testdrive.operation.next
+import com.bumble.appyx.demos.sample1.InteractionTarget.Child1
+import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer
+import com.bumble.appyx.interactions.gesture.GestureReferencePoint
+import com.bumble.appyx.interactions.model.transition.Operation.Mode.IMMEDIATE
+import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup
+
+enum class InteractionTarget {
+ Child1
+}
+
+@Composable
+fun Sample1(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember { TestDriveModel(Child1, null) }
+ val testDrive = remember {
+ TestDrive(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { Sample1Visualisation(it) },
+ gestureFactory = { Sample1Visualisation.Gestures(it) }
+ )
+ }
+
+ AppyxComponentSetup(testDrive)
+
+ Box(
+ modifier = modifier,
+ ) {
+ Background(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ model = model
+ )
+ Box(
+ modifier = Modifier.padding(24.dp, 24.dp)
+ ) {
+ ModelUi(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ testDrive = testDrive,
+ model = model
+ )
+ Controls(
+ testDrive = testDrive
+ )
+ }
+ }
+}
+
+@Composable
+fun Background(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ model: TestDriveModel,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ val output = model.output.collectAsState()
+ val currentTarget = output.value.currentTargetState.elementState
+ val backgroundColor1 = animateColorAsState(
+ when (currentTarget) {
+ A -> color_neutral1
+ B -> color_neutral2
+ C -> color_neutral3
+ D -> color_neutral4
+ }
+ )
+
+ Box(
+ modifier
+ .zIndex(0f)
+ .background(
+ ShaderBrush(
+ LinearGradientShader(
+ from = Offset.Zero,
+ to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()),
+ colors = listOf(color_bright, backgroundColor1.value)
+ )
+ )
+ )
+ )
+}
+
+@Composable
+fun ModelUi(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ testDrive: TestDrive,
+ model: TestDriveModel,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ val output = model.output.collectAsState()
+ val currentTarget = output.value.currentTargetState.elementState
+ val backgroundColor1 = animateColorAsState(
+ when (currentTarget) {
+ A -> color_neutral1
+ B -> color_neutral2
+ C -> color_neutral3
+ D -> color_neutral4
+ }
+ )
+
+ AppyxInteractionsContainer(
+ appyxComponent = testDrive,
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ gestureRelativeTo = GestureReferencePoint.Element
+ ) {
+ Box(
+ modifier = Modifier
+ .size(60.dp)
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = model.output.collectAsState().value.currentTargetState.elementState.name,
+ fontSize = 24.sp,
+ color = Color.White
+ )
+ }
+ }
+}
+
+@Composable
+private fun Controls(
+ testDrive: TestDrive,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ Column(
+ modifier = modifier.zIndex(1f),
+ verticalArrangement = Arrangement.Bottom,
+ horizontalAlignment = CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .background(color_primary, shape = RoundedCornerShape(4.dp))
+ .clickable {
+ testDrive.next(
+ mode = IMMEDIATE, animationSpec = spring(
+ stiffness = Spring.StiffnessMedium,
+ dampingRatio = Spring.DampingRatioLowBouncy,
+ )
+ )
+ }
+ .padding(horizontal = 18.dp, vertical = 9.dp)
+ ) {
+ Text("Next")
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1Visualisation.kt b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1Visualisation.kt
new file mode 100644
index 000000000..8fd3e1764
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/Sample1Visualisation.kt
@@ -0,0 +1,148 @@
+package com.bumble.appyx.demos.sample1
+
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.components.internal.testdrive.operation.MoveTo
+import com.bumble.appyx.interactions.ui.context.TransitionBounds
+import com.bumble.appyx.interactions.ui.context.UiContext
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNLEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPRIGHT
+import com.bumble.appyx.interactions.gesture.Gesture
+import com.bumble.appyx.interactions.gesture.GestureFactory
+import com.bumble.appyx.interactions.gesture.dragDirection8
+import com.bumble.appyx.interactions.ui.DefaultAnimationSpec
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomEnd
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopEnd
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopStart
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState
+import com.bumble.appyx.transitionmodel.BaseVisualisation
+import com.bumble.appyx.utils.multiplatform.AppyxLogger
+
+class Sample1Visualisation(
+ uiContext: UiContext,
+ uiAnimationSpec: SpringSpec = DefaultAnimationSpec
+) : BaseVisualisation, TargetUiState, MutableUiState>(
+ uiContext = uiContext,
+ defaultAnimationSpec = uiAnimationSpec,
+) {
+ override fun TestDriveModel.State.toUiTargets():
+ List> =
+ listOf(
+ MatchedTargetUiState(element, elementState.toTargetUiState()).also {
+ AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}")
+ }
+ )
+
+ companion object {
+ val bottomOffset = DpOffset(0.dp, (-50).dp)
+
+ fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState =
+ when (this) {
+ A -> topLeftCorner
+ B -> topRightCorner
+ C -> bottomRightCorner
+ D -> bottomLeftCorner
+ }
+
+ private val topLeftCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(TopStart),
+ positionOffset = PositionOffset.Target(DpOffset.Zero),
+ backgroundColor = BackgroundColor.Target(color_primary)
+ )
+
+ private val topRightCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(TopEnd),
+ positionOffset = PositionOffset.Target(DpOffset.Zero),
+ backgroundColor = BackgroundColor.Target(color_dark)
+ )
+
+ private val bottomRightCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(BottomEnd),
+ positionOffset = PositionOffset.Target(bottomOffset),
+ backgroundColor = BackgroundColor.Target(color_secondary)
+ )
+
+ private val bottomLeftCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(InsideAlignment.BottomStart),
+ positionOffset = PositionOffset.Target(bottomOffset),
+ backgroundColor = BackgroundColor.Target(color_tertiary)
+ )
+ }
+
+ override fun mutableUiStateFor(
+ uiContext: UiContext,
+ targetUiState: TargetUiState
+ ): MutableUiState =
+ targetUiState.toMutableUiState(uiContext)
+
+ class Gestures(
+ private val transitionBounds: TransitionBounds,
+ ) : GestureFactory> {
+
+ @Suppress("ComplexMethod")
+ override fun createGesture(
+ state: TestDriveModel.State,
+ delta: Offset,
+ density: Density
+ ): Gesture> {
+ // FIXME quick fix – 60.dp is the assumed element size, connect it to real value
+ // TODO properly – automate this whole calculation based on .onPlaced centers of targetUiStates
+ val maxX = with(density) {
+ (transitionBounds.widthDp - 60.dp).toPx()
+ }
+ val maxY = with(density) {
+ (transitionBounds.heightDp + bottomOffset.y - 60.dp).toPx()
+ }
+
+ val direction = dragDirection8(delta)
+ return when (state.elementState) {
+ A -> when (direction) {
+ RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f))
+ DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY))
+ DOWN -> Gesture(MoveTo(D), Offset(0f, maxY))
+ else -> Gesture.Noop()
+ }
+
+ B -> when (direction) {
+ DOWN -> Gesture(MoveTo(C), Offset(0f, maxY))
+ DOWNLEFT -> Gesture(MoveTo(D), Offset(-maxX, maxY))
+ LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f))
+ else -> Gesture.Noop()
+ }
+
+ C -> when (direction) {
+ LEFT -> Gesture(MoveTo(D), Offset(-maxX, 0f))
+ UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY))
+ UP -> Gesture(MoveTo(B), Offset(0f, -maxY))
+ else -> Gesture.Noop()
+ }
+
+ D -> when (direction) {
+ UP -> Gesture(MoveTo(A), Offset(0f, -maxY))
+ UPRIGHT -> Gesture(MoveTo(B), Offset(maxX, -maxY))
+ RIGHT -> Gesture(MoveTo(C), Offset(maxX, 0f))
+ else -> Gesture.Noop()
+ }
+ }
+ }
+ }
+}
+
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/TargetUiState.kt b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/TargetUiState.kt
new file mode 100644
index 000000000..4312ea1b6
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/TargetUiState.kt
@@ -0,0 +1,16 @@
+package com.bumble.appyx.demos.sample1
+
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs
+
+@Suppress("unused")
+@MutableUiStateSpecs
+class TargetUiState(
+ val positionAlignment: PositionAlignment.Target,
+ val positionOffset: PositionOffset.Target,
+ val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10),
+ val backgroundColor: BackgroundColor.Target,
+)
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/main.js.kt b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/main.js.kt
new file mode 100644
index 000000000..ecd275657
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample1/main.js.kt
@@ -0,0 +1,57 @@
+package com.bumble.appyx.demos.sample1
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+
+val color_bright = Color(0xFFFFFFFF)
+val color_dark = Color(0xFF353535)
+val color_primary = Color(0xFFFFC629)
+val color_secondary = Color(0xFFFE9763)
+val color_tertiary = Color(0xFF855353)
+val color_neutral1 = Color(0xFFD2D7DF)
+val color_neutral2 = Color(0xFF8A897C)
+val color_neutral3 = Color(0xFFD9E8ED)
+val color_neutral4 = Color(0xFFBEA489)
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ Sample1(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..5ca3e007b
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample1/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/interactions/sample2/web/build.gradle.kts
index 755a322db..d3ae7a978 100644
--- a/demos/mkdocs/appyx-interactions/interactions/sample2/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-interactions-sample2-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/interactions/sample2/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2.kt b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2.kt
new file mode 100644
index 000000000..be07cd535
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2.kt
@@ -0,0 +1,190 @@
+@file:Suppress("MatchingDeclarationName")
+
+package com.bumble.appyx.demos.sample2
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+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.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.LinearGradientShader
+import androidx.compose.ui.graphics.ShaderBrush
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
+import com.bumble.appyx.components.internal.testdrive.TestDrive
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.components.internal.testdrive.operation.next
+import com.bumble.appyx.demos.sample2.InteractionTarget.Child1
+import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer
+import com.bumble.appyx.interactions.gesture.GestureReferencePoint
+import com.bumble.appyx.interactions.model.transition.Operation.Mode.IMMEDIATE
+import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup
+
+enum class InteractionTarget {
+ Child1
+}
+
+@Composable
+fun Sample2(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember { TestDriveModel(Child1, null) }
+ val testDrive = remember {
+ TestDrive(
+ scope = coroutineScope,
+ model = model,
+ visualisation = { Sample2Visualisation(it) },
+ gestureFactory = { Sample2Visualisation.Gestures(it) }
+ )
+ }
+
+ AppyxComponentSetup(testDrive)
+
+ Box(
+ modifier = modifier,
+ ) {
+ Background(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ model = model
+ )
+ Box(
+ modifier = Modifier.padding(24.dp, 24.dp)
+ ) {
+ ModelUi(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ testDrive = testDrive,
+ model = model
+ )
+ Controls(
+ testDrive = testDrive
+ )
+ }
+ }
+}
+
+@Composable
+fun Background(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ model: TestDriveModel,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ val output = model.output.collectAsState()
+ val currentTarget = output.value.currentTargetState.elementState
+ val backgroundColor1 = animateColorAsState(
+ when (currentTarget) {
+ A -> color_neutral1
+ B -> color_neutral2
+ C -> color_neutral3
+ D -> color_neutral4
+ }
+ )
+
+ Box(
+ modifier
+ .zIndex(0f)
+ .background(
+ ShaderBrush(
+ LinearGradientShader(
+ from = Offset.Zero,
+ to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()),
+ colors = listOf(color_bright, backgroundColor1.value)
+ )
+ )
+ )
+ )
+}
+
+@Composable
+fun ModelUi(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ testDrive: TestDrive,
+ model: TestDriveModel,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ val output = model.output.collectAsState()
+ val currentTarget = output.value.currentTargetState.elementState
+ val backgroundColor1 = animateColorAsState(
+ when (currentTarget) {
+ A -> color_neutral1
+ B -> color_neutral2
+ C -> color_neutral3
+ D -> color_neutral4
+ }
+ )
+
+ AppyxInteractionsContainer(
+ appyxComponent = testDrive,
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ gestureRelativeTo = GestureReferencePoint.Element
+ ) {
+ Box(
+ modifier = Modifier
+ .size(60.dp)
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = model.output.collectAsState().value.currentTargetState.elementState.name,
+ fontSize = 24.sp,
+ color = Color.White
+ )
+ }
+ }
+}
+
+@Composable
+private fun Controls(
+ testDrive: TestDrive,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ Column(
+ modifier = modifier.zIndex(1f),
+ verticalArrangement = Arrangement.Bottom,
+ horizontalAlignment = CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .background(color_primary, shape = RoundedCornerShape(4.dp))
+ .clickable {
+ testDrive.next(
+ mode = IMMEDIATE, animationSpec = spring(
+ stiffness = Spring.StiffnessLow,
+ dampingRatio = Spring.DampingRatioLowBouncy,
+ )
+ )
+ }
+ .padding(horizontal = 18.dp, vertical = 9.dp)
+ ) {
+ Text("Next")
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2Visualisation.kt b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2Visualisation.kt
new file mode 100644
index 000000000..2d7474fb3
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/Sample2Visualisation.kt
@@ -0,0 +1,154 @@
+package com.bumble.appyx.demos.sample2
+
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.components.internal.testdrive.operation.MoveTo
+import com.bumble.appyx.interactions.ui.context.TransitionBounds
+import com.bumble.appyx.interactions.ui.context.UiContext
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNLEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPRIGHT
+import com.bumble.appyx.interactions.gesture.Gesture
+import com.bumble.appyx.interactions.gesture.GestureFactory
+import com.bumble.appyx.interactions.gesture.dragDirection8
+import com.bumble.appyx.interactions.ui.DefaultAnimationSpec
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.RotationZ
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomEnd
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopEnd
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopStart
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState
+import com.bumble.appyx.transitionmodel.BaseVisualisation
+import com.bumble.appyx.utils.multiplatform.AppyxLogger
+
+class Sample2Visualisation(
+ uiContext: UiContext,
+ uiAnimationSpec: SpringSpec = DefaultAnimationSpec
+) : BaseVisualisation, TargetUiState, MutableUiState>(
+ uiContext = uiContext,
+ defaultAnimationSpec = uiAnimationSpec,
+) {
+ override fun TestDriveModel.State.toUiTargets():
+ List> =
+ listOf(
+ MatchedTargetUiState(element, elementState.toTargetUiState()).also {
+ AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}")
+ }
+ )
+
+ companion object {
+ val bottomOffset = DpOffset(0.dp, (-50).dp)
+
+ fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState =
+ when (this) {
+ A -> topLeftCorner
+ B -> topRightCorner
+ C -> bottomRightCorner
+ D -> bottomLeftCorner
+ }
+
+ private val topLeftCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(TopStart),
+ positionOffset = PositionOffset.Target(DpOffset.Zero),
+ rotationZ = RotationZ.Target(0f),
+ backgroundColor = BackgroundColor.Target(color_primary)
+ )
+
+ private val topRightCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(TopEnd),
+ positionOffset = PositionOffset.Target(DpOffset.Zero),
+ rotationZ = RotationZ.Target(180f),
+ backgroundColor = BackgroundColor.Target(color_dark)
+ )
+
+ private val bottomRightCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(BottomEnd),
+ positionOffset = PositionOffset.Target(bottomOffset),
+ rotationZ = RotationZ.Target(270f),
+ backgroundColor = BackgroundColor.Target(color_secondary)
+ )
+
+ private val bottomLeftCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(InsideAlignment.BottomStart),
+ positionOffset = PositionOffset.Target(bottomOffset),
+ rotationZ = RotationZ.Target(540f),
+ backgroundColor = BackgroundColor.Target(color_tertiary)
+ )
+ }
+
+ override fun mutableUiStateFor(
+ uiContext: UiContext,
+ targetUiState: TargetUiState
+ ): MutableUiState =
+ targetUiState.toMutableUiState(uiContext)
+
+
+ class Gestures(
+ private val transitionBounds: TransitionBounds,
+ ) : GestureFactory> {
+
+ @Suppress("ComplexMethod")
+ override fun createGesture(
+ state: TestDriveModel.State,
+ delta: Offset,
+ density: Density
+ ): Gesture> {
+ // FIXME 60.dp is the assumed element size, connect it to real value
+ // TODO automate this whole calculation based on .onPlaced centers of targetUiStates
+ val maxX = with(density) {
+ (transitionBounds.widthDp - 60.dp).toPx()
+ }
+ val maxY = with(density) {
+ (transitionBounds.heightDp + bottomOffset.y - 60.dp).toPx()
+ }
+
+ val direction = dragDirection8(delta)
+ return when (state.elementState) {
+ A -> when (direction) {
+ RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f))
+ DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY))
+ DOWN -> Gesture(MoveTo(D), Offset(0f, maxY))
+ else -> Gesture.Noop()
+ }
+
+ B -> when (direction) {
+ DOWN -> Gesture(MoveTo(C), Offset(0f, maxY))
+ DOWNLEFT -> Gesture(MoveTo(D), Offset(-maxX, maxY))
+ LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f))
+ else -> Gesture.Noop()
+ }
+
+ C -> when (direction) {
+ LEFT -> Gesture(MoveTo(D), Offset(-maxX, 0f))
+ UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY))
+ UP -> Gesture(MoveTo(B), Offset(0f, -maxY))
+ else -> Gesture.Noop()
+ }
+
+ D -> when (direction) {
+ UP -> Gesture(MoveTo(A), Offset(0f, -maxY))
+ UPRIGHT -> Gesture(MoveTo(B), Offset(maxX, -maxY))
+ RIGHT -> Gesture(MoveTo(C), Offset(maxX, 0f))
+ else -> Gesture.Noop()
+ }
+ }
+ }
+ }
+}
+
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/TargetUiState.kt b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/TargetUiState.kt
new file mode 100644
index 000000000..bd86cc384
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/TargetUiState.kt
@@ -0,0 +1,18 @@
+package com.bumble.appyx.demos.sample2
+
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.RotationZ
+import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs
+
+@Suppress("unused")
+@MutableUiStateSpecs
+class TargetUiState(
+ val positionAlignment: PositionAlignment.Target,
+ val positionOffset: PositionOffset.Target,
+ val rotationZ: RotationZ.Target,
+ val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10),
+ val backgroundColor: BackgroundColor.Target,
+)
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/main.js.kt b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/main.js.kt
new file mode 100644
index 000000000..5f074c3c2
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample2/main.js.kt
@@ -0,0 +1,57 @@
+package com.bumble.appyx.demos.sample2
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+
+val color_bright = Color(0xFFFFFFFF)
+val color_dark = Color(0xFF353535)
+val color_primary = Color(0xFFFFC629)
+val color_secondary = Color(0xFFFE9763)
+val color_tertiary = Color(0xFF855353)
+val color_neutral1 = Color(0xFFD2D7DF)
+val color_neutral2 = Color(0xFF8A897C)
+val color_neutral3 = Color(0xFFD9E8ED)
+val color_neutral4 = Color(0xFFBEA489)
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ Sample2(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..5ca3e007b
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample2/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/build.gradle.kts b/demos/mkdocs/appyx-interactions/interactions/sample3/web/build.gradle.kts
index dadf32aa0..192ffe498 100644
--- a/demos/mkdocs/appyx-interactions/interactions/sample3/web/build.gradle.kts
+++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -10,6 +12,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-interactions-sample3-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
@@ -36,4 +52,5 @@ compose.experimental {
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/karma.config.d/wasm/config.js b/demos/mkdocs/appyx-interactions/interactions/sample3/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3.kt b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3.kt
new file mode 100644
index 000000000..c94fa7ad5
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3.kt
@@ -0,0 +1,257 @@
+@file:Suppress("MatchingDeclarationName")
+package com.bumble.appyx.demos.sample3
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+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.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.LinearGradientShader
+import androidx.compose.ui.graphics.ShaderBrush
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
+import com.bumble.appyx.components.internal.testdrive.TestDrive
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.components.internal.testdrive.operation.next
+import com.bumble.appyx.demos.sample3.InteractionTarget.Child1
+import com.bumble.appyx.demos.sample3.Sample3Visualisation.Companion.toTargetUiState
+import com.bumble.appyx.interactions.composable.AppyxInteractionsContainer
+import com.bumble.appyx.interactions.gesture.GestureReferencePoint
+import com.bumble.appyx.interactions.model.transition.Keyframes
+import com.bumble.appyx.interactions.model.transition.Operation.Mode.IMMEDIATE
+import com.bumble.appyx.interactions.model.transition.Operation.Mode.KEYFRAME
+import com.bumble.appyx.interactions.model.transition.Update
+import com.bumble.appyx.interactions.ui.helper.AppyxComponentSetup
+
+enum class InteractionTarget {
+ Child1
+}
+
+@Composable
+fun Sample3(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val model = remember { TestDriveModel(Child1, null) }
+ val testDrive = remember {
+ TestDrive(
+ scope = coroutineScope,
+ model = model,
+ progressAnimationSpec = spring(
+ stiffness = Spring.StiffnessVeryLow / 10,
+ visibilityThreshold = 0.001f
+ ),
+ visualisation = { Sample3Visualisation(it) },
+ gestureFactory = { Sample3Visualisation.Gestures(it) }
+ )
+ }
+
+ AppyxComponentSetup(testDrive)
+
+ val output = model.output.collectAsState().value
+ val currentTarget: State?> =
+ when (output) {
+ is Keyframes -> output.currentSegmentTargetStateFlow.collectAsState(null)
+ is Update -> remember(output) { mutableStateOf(output.currentTargetState) }
+ }
+ val index = when (output) {
+ is Keyframes -> output.currentIndex
+ is Update -> null
+ }
+
+ Box(
+ modifier = modifier,
+ ) {
+ Background(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ currentTarget = currentTarget.value
+ )
+ Box(
+ modifier = Modifier.padding(24.dp, 24.dp)
+ ) {
+ Target(
+ boxScope = this,
+ currentTarget = currentTarget.value,
+ index = index
+ )
+ ModelUi(
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ testDrive = testDrive,
+ model = model
+ )
+ Controls(
+ testDrive = testDrive
+ )
+ }
+ }
+}
+
+@Composable
+fun Background(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ currentTarget: TestDriveModel.State?,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ val backgroundColor1 = animateColorAsState(
+ when (currentTarget?.elementState) {
+ A -> color_neutral1
+ B -> color_neutral2
+ C -> color_neutral3
+ D -> color_neutral4
+ null -> color_bright
+ },
+ animationSpec = spring(stiffness = Spring.StiffnessVeryLow)
+ )
+
+ Box(
+ modifier
+ .zIndex(0f)
+ .background(
+ ShaderBrush(
+ LinearGradientShader(
+ from = Offset.Zero,
+ to = Offset(screenWidthPx.toFloat(), screenHeightPx.toFloat()),
+ colors = listOf(color_bright, backgroundColor1.value)
+ )
+ )
+ )
+ )
+}
+
+@Composable
+fun Target(
+ boxScope: BoxScope,
+ currentTarget: TestDriveModel.State?,
+ index: Int?,
+ modifier: Modifier = Modifier
+) {
+ val targetUiState = currentTarget?.elementState?.toTargetUiState()
+ targetUiState?.let {
+ Box(
+ modifier = with(boxScope) {
+ modifier
+ .size(60.dp)
+ .align(targetUiState.positionAlignment.value)
+ .offset(
+ x = targetUiState.positionOffset.value.offset.x,
+ y = targetUiState.positionOffset.value.offset.y
+ )
+ .alpha(0.35f)
+ .background(
+ color = targetUiState.backgroundColor.value,
+ shape = RoundedCornerShape(targetUiState.roundedCorners.value)
+ )
+ }
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = index?.toString() ?: "X",
+ fontSize = 24.sp,
+ color = Color.White
+ )
+ }
+ }
+}
+
+@Composable
+fun ModelUi(
+ screenWidthPx: Int,
+ screenHeightPx: Int,
+ testDrive: TestDrive,
+ model: TestDriveModel,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ AppyxInteractionsContainer(
+ appyxComponent = testDrive,
+ screenWidthPx = screenWidthPx,
+ screenHeightPx = screenHeightPx,
+ gestureRelativeTo = GestureReferencePoint.Element,
+ modifier = modifier
+ ) {
+ Box(
+ modifier = Modifier.size(60.dp)
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = model.output.collectAsState().value.currentTargetState.elementState.name,
+ fontSize = 24.sp,
+ color = Color.White
+ )
+ }
+ }
+}
+
+@Composable
+private fun Controls(
+ testDrive: TestDrive,
+ modifier: Modifier = Modifier.fillMaxSize()
+) {
+ Column(
+ modifier = modifier.zIndex(1f),
+ verticalArrangement = Arrangement.Bottom,
+ horizontalAlignment = CenterHorizontally
+ ) {
+ Row {
+ Box(
+ modifier = Modifier
+ .background(color_primary, shape = RoundedCornerShape(4.dp))
+ .clickable { testDrive.next(mode = KEYFRAME) }
+ .padding(horizontal = 18.dp, vertical = 9.dp)
+ ) {
+ Text("Keyframe")
+ }
+ Spacer(Modifier.width(16.dp))
+ Box(
+ modifier = Modifier
+ .background(color_primary, shape = RoundedCornerShape(4.dp))
+ .clickable {
+ testDrive.next(
+ mode = IMMEDIATE, spring(
+ stiffness = Spring.StiffnessVeryLow,
+ dampingRatio = Spring.DampingRatioMediumBouncy
+ )
+ )
+ }
+ .padding(horizontal = 18.dp, vertical = 9.dp)
+ ) {
+ Text("Immediate")
+ }
+ }
+
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3Visualisation.kt b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3Visualisation.kt
new file mode 100644
index 000000000..00dd57a56
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/Sample3Visualisation.kt
@@ -0,0 +1,148 @@
+package com.bumble.appyx.demos.sample3
+
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.A
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.B
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.C
+import com.bumble.appyx.components.internal.testdrive.TestDriveModel.State.ElementState.D
+import com.bumble.appyx.components.internal.testdrive.operation.MoveTo
+import com.bumble.appyx.interactions.ui.context.TransitionBounds
+import com.bumble.appyx.interactions.ui.context.UiContext
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWN
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNLEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.DOWNRIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.LEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.RIGHT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UP
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPLEFT
+import com.bumble.appyx.interactions.gesture.Drag.Direction8.UPRIGHT
+import com.bumble.appyx.interactions.gesture.Gesture
+import com.bumble.appyx.interactions.gesture.GestureFactory
+import com.bumble.appyx.interactions.gesture.dragDirection8
+import com.bumble.appyx.interactions.ui.DefaultAnimationSpec
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomEnd
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.BottomStart
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopEnd
+import com.bumble.appyx.interactions.ui.property.impl.position.BiasAlignment.InsideAlignment.Companion.TopStart
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState
+import com.bumble.appyx.transitionmodel.BaseVisualisation
+import com.bumble.appyx.utils.multiplatform.AppyxLogger
+
+class Sample3Visualisation(
+ uiContext: UiContext,
+ uiAnimationSpec: SpringSpec = DefaultAnimationSpec
+) : BaseVisualisation, TargetUiState, MutableUiState>(
+ uiContext = uiContext,
+ defaultAnimationSpec = uiAnimationSpec,
+) {
+ override fun TestDriveModel.State.toUiTargets():
+ List> =
+ listOf(
+ MatchedTargetUiState(element, elementState.toTargetUiState()).also {
+ AppyxLogger.d("TestDrive", "Matched $elementState -> UiState: ${it.targetUiState}")
+ }
+ )
+
+ companion object {
+ val bottomOffset = DpOffset(0.dp, (-50).dp)
+
+ fun TestDriveModel.State.ElementState.toTargetUiState(): TargetUiState =
+ when (this) {
+ A -> topLeftCorner
+ B -> topRightCorner
+ C -> bottomRightCorner
+ D -> bottomLeftCorner
+ }
+
+ private val topLeftCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(TopStart),
+ positionOffset = PositionOffset.Target(DpOffset.Zero),
+ backgroundColor = BackgroundColor.Target(color_primary)
+ )
+
+ private val topRightCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(TopEnd),
+ positionOffset = PositionOffset.Target(DpOffset.Zero),
+ backgroundColor = BackgroundColor.Target(color_dark)
+ )
+
+ private val bottomRightCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(BottomEnd),
+ positionOffset = PositionOffset.Target(bottomOffset),
+ backgroundColor = BackgroundColor.Target(color_secondary)
+ )
+
+ private val bottomLeftCorner = TargetUiState(
+ positionAlignment = PositionAlignment.Target(BottomStart),
+ positionOffset = PositionOffset.Target(bottomOffset),
+ backgroundColor = BackgroundColor.Target(color_tertiary)
+ )
+ }
+
+ override fun mutableUiStateFor(
+ uiContext: UiContext,
+ targetUiState: TargetUiState
+ ): MutableUiState =
+ targetUiState.toMutableUiState(uiContext)
+
+ class Gestures(
+ private val transitionBounds: TransitionBounds,
+ ) : GestureFactory> {
+
+ @Suppress("ComplexMethod")
+ override fun createGesture(
+ state: TestDriveModel.State,
+ delta: Offset,
+ density: Density
+ ): Gesture> {
+ // FIXME 60.dp is the assumed element size, connect it to real value
+ // TODO automate this whole calculation based on .onPlaced centers of targetUiStates
+ val maxX = with(density) {
+ (transitionBounds.widthDp - 60.dp).toPx()
+ }
+ val maxY = with(density) {
+ (transitionBounds.heightDp + bottomOffset.y - 60.dp).toPx()
+ }
+
+ val direction = dragDirection8(delta)
+ return when (state.elementState) {
+ A -> when (direction) {
+ RIGHT -> Gesture(MoveTo(B), Offset(maxX, 0f))
+ DOWNRIGHT -> Gesture(MoveTo(C), Offset(maxX, maxY))
+ DOWN -> Gesture(MoveTo(D), Offset(0f, maxY))
+ else -> Gesture.Noop()
+ }
+
+ B -> when (direction) {
+ DOWN -> Gesture(MoveTo(C), Offset(0f, maxY))
+ DOWNLEFT -> Gesture(MoveTo(D), Offset(-maxX, maxY))
+ LEFT -> Gesture(MoveTo(A), Offset(-maxX, 0f))
+ else -> Gesture.Noop()
+ }
+
+ C -> when (direction) {
+ LEFT -> Gesture(MoveTo(D), Offset(-maxX, 0f))
+ UPLEFT -> Gesture(MoveTo(A), Offset(-maxX, -maxY))
+ UP -> Gesture(MoveTo(B), Offset(0f, -maxY))
+ else -> Gesture.Noop()
+ }
+
+ D -> when (direction) {
+ UP -> Gesture(MoveTo(A), Offset(0f, -maxY))
+ UPRIGHT -> Gesture(MoveTo(B), Offset(maxX, -maxY))
+ RIGHT -> Gesture(MoveTo(C), Offset(maxX, 0f))
+ else -> Gesture.Noop()
+ }
+ }
+ }
+ }
+}
+
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/TargetUiState.kt b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/TargetUiState.kt
new file mode 100644
index 000000000..9e00c3688
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/TargetUiState.kt
@@ -0,0 +1,16 @@
+package com.bumble.appyx.demos.sample3
+
+import com.bumble.appyx.interactions.ui.property.impl.BackgroundColor
+import com.bumble.appyx.interactions.ui.property.impl.RoundedCorners
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionAlignment
+import com.bumble.appyx.interactions.ui.property.impl.position.PositionOffset
+import com.bumble.appyx.interactions.ui.state.MutableUiStateSpecs
+
+@Suppress("unused")
+@MutableUiStateSpecs
+class TargetUiState(
+ val positionAlignment: PositionAlignment.Target,
+ val positionOffset: PositionOffset.Target,
+ val roundedCorners: RoundedCorners.Target = RoundedCorners.Target(10),
+ val backgroundColor: BackgroundColor.Target,
+)
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/main.js.kt b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/main.js.kt
new file mode 100644
index 000000000..ae3c04bf4
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sample3/main.js.kt
@@ -0,0 +1,57 @@
+package com.bumble.appyx.demos.sample3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Surface
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.appyxSample
+
+val color_bright = Color(0xFFFFFFFF)
+val color_dark = Color(0xFF353535)
+val color_primary = Color(0xFFFFC629)
+val color_secondary = Color(0xFFFE9763)
+val color_tertiary = Color(0xFF855353)
+val color_neutral1 = Color(0xFFD2D7DF)
+val color_neutral2 = Color(0xFF8A897C)
+val color_neutral3 = Color(0xFFD9E8ED)
+val color_neutral4 = Color(0xFFBEA489)
+
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ appyxSample {
+ CanvasBasedWindow("Appyx") {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { size = it }
+ ) {
+ if (size != IntSize.Zero) {
+ Sample3(
+ screenWidthPx = size.width,
+ screenHeightPx = size.height,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color_dark)
+ .padding(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/index.html b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..5ca3e007b
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Appyx Interactions
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/styles.css b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..f8b13d234
--- /dev/null
+++ b/demos/mkdocs/appyx-interactions/interactions/sample3/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,19 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
+
+@media (max-width: 511px) {
+ #ComposeTarget {
+ scale: 0.5;
+ transform-origin: 0 0;
+ }
+}
diff --git a/demos/mkdocs/common/build.gradle.kts b/demos/mkdocs/common/build.gradle.kts
index 1bb32ce51..e92a3c463 100644
--- a/demos/mkdocs/common/build.gradle.kts
+++ b/demos/mkdocs/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -9,6 +11,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "demos-mkdocs-common-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
diff --git a/demos/mkdocs/common/karma.config.d/wasm/config.js b/demos/mkdocs/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/mkdocs/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/mkdocs/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/AppyxSample.kt b/demos/mkdocs/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/AppyxSample.kt
new file mode 100644
index 000000000..6386e9029
--- /dev/null
+++ b/demos/mkdocs/common/src/wasmJsMain/kotlin/com/bumble/appyx/demos/AppyxSample.kt
@@ -0,0 +1,65 @@
+package com.bumble.appyx.demos
+
+import kotlinx.browser.document
+import org.w3c.dom.get
+
+private val LOADER_STYLES = """
+ .loader {
+ width: 48px;
+ height: 48px;
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ border: 5px solid rgba(255, 227, 0, 255);
+ border-bottom-color: transparent;
+ border-radius: 50%;
+ animation: rotation 1s linear 1s infinite;
+ visibility: hidden;
+ margin: auto;
+ }
+
+ @keyframes rotation {
+ 0% {
+ visibility: visible;
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+""".trimIndent()
+
+external fun onWasmReady(onReady: () -> Unit)
+
+fun appyxSample(
+ block: () -> Unit,
+) {
+ appendLoaderStyles()
+ appendLoaderElement()
+ onWasmReady {
+ block()
+ removeLoaderElement()
+ }
+}
+
+private fun appendLoaderStyles() {
+ val head = document.head ?: document.getElementsByTagName("head")[0]
+ head?.apply {
+ val style = document.createElement("style")
+ head.appendChild(style)
+ style.appendChild(document.createTextNode(LOADER_STYLES))
+ }
+}
+
+private fun appendLoaderElement() {
+ val composeTarget = document.getElementById("ComposeTarget")
+ val loader = document.createElement("div")
+ loader.className = "loader"
+ composeTarget?.parentNode?.appendChild(loader)
+}
+
+private fun removeLoaderElement() {
+ val loader = document.getElementsByClassName("loader")[0]
+ loader?.parentNode?.removeChild(loader)
+}
\ No newline at end of file
diff --git a/demos/sandbox-appyx-navigation/common/build.gradle.kts b/demos/sandbox-appyx-navigation/common/build.gradle.kts
index 5762b9f39..3ed22313b 100644
--- a/demos/sandbox-appyx-navigation/common/build.gradle.kts
+++ b/demos/sandbox-appyx-navigation/common/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -23,6 +25,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "demo-sandbox-appyx-navigation-common"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "demo-sandbox-appyx-navigation-common-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
iosArm64()
@@ -88,11 +106,16 @@ android {
}
}
+compose.experimental {
+ web.application {}
+}
+
dependencies {
add("kspCommonMainMetadata", project(":ksp:appyx-processor"))
add("kspAndroid", project(":ksp:appyx-processor"))
add("kspDesktop", project(":ksp:appyx-processor"))
add("kspJs", project(":ksp:appyx-processor"))
+ add("kspWasmJs", project(":ksp:appyx-processor"))
add("kspIosArm64", project(":ksp:appyx-processor"))
add("kspIosX64", project(":ksp:appyx-processor"))
add("kspIosSimulatorArm64", project(":ksp:appyx-processor"))
diff --git a/demos/sandbox-appyx-navigation/common/karma.config.d/wasm/config.js b/demos/sandbox-appyx-navigation/common/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/sandbox-appyx-navigation/common/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/sandbox-appyx-navigation/web/build.gradle.kts b/demos/sandbox-appyx-navigation/web/build.gradle.kts
index 6d7ea56ed..f946ce471 100644
--- a/demos/sandbox-appyx-navigation/web/build.gradle.kts
+++ b/demos/sandbox-appyx-navigation/web/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("org.jetbrains.compose")
@@ -9,6 +11,20 @@ kotlin {
browser()
binaries.executable()
}
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "appyx-demos-sandbox-navigation-web-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
val commonMain by getting {
dependencies {
diff --git a/demos/sandbox-appyx-navigation/web/karma.config.d/wasm/config.js b/demos/sandbox-appyx-navigation/web/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/demos/sandbox-appyx-navigation/web/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/demos/sandbox-appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sandbox/navigation/Main.kt b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sandbox/navigation/Main.kt
new file mode 100644
index 000000000..19753aae1
--- /dev/null
+++ b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/kotlin/com/bumble/appyx/demos/sandbox/navigation/Main.kt
@@ -0,0 +1,96 @@
+package com.bumble.appyx.demos.sandbox.navigation
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.CanvasBasedWindow
+import com.bumble.appyx.demos.sandbox.navigation.node.container.MainNavNode
+import com.bumble.appyx.demos.sandbox.navigation.ui.AppyxSampleAppTheme
+import com.bumble.appyx.navigation.integration.ScreenSize
+import com.bumble.appyx.navigation.integration.WebNodeHost
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+external fun onWasmReady(onReady: () -> Unit)
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+ val events: Channel = Channel()
+ onWasmReady {
+ CanvasBasedWindow("Navigation Demo") {
+ val requester = remember { FocusRequester() }
+ var hasFocus by remember { mutableStateOf(false) }
+
+ var screenSize by remember { mutableStateOf(ScreenSize(0.dp, 0.dp)) }
+ val eventScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
+
+ AppyxSampleAppTheme {
+ Surface(
+ color = MaterialTheme.colorScheme.background,
+ modifier = Modifier
+ .fillMaxSize()
+ .onSizeChanged { screenSize = ScreenSize(it.width.dp, it.height.dp) }
+ .onKeyEvent {
+ onKeyEvent(it, events, eventScope)
+ }
+ .focusRequester(requester)
+ .focusable()
+ .onFocusChanged { hasFocus = it.hasFocus }
+ ) {
+ WebNodeHost(
+ screenSize = screenSize,
+ onBackPressedEvents = events.receiveAsFlow(),
+ ) { nodeContext ->
+ MainNavNode(
+ nodeContext = nodeContext,
+ )
+ }
+ }
+
+ if (!hasFocus) {
+ LaunchedEffect(Unit) {
+ requester.requestFocus()
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private fun onKeyEvent(
+ keyEvent: KeyEvent,
+ events: Channel,
+ coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
+): Boolean =
+ when {
+ keyEvent.type == KeyEventType.KeyUp && keyEvent.key == Key.Backspace -> {
+ coroutineScope.launch { events.send(Unit) }
+ true
+ }
+
+ else -> false
+ }
diff --git a/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/index.html b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/index.html
new file mode 100644
index 000000000..b09444ce6
--- /dev/null
+++ b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Navigation Demo
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/styles.css b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/styles.css
new file mode 100644
index 000000000..8655f2e76
--- /dev/null
+++ b/demos/sandbox-appyx-navigation/web/src/wasmJsMain/resources/styles.css
@@ -0,0 +1,12 @@
+#root {
+ width: 100%;
+ height: 100vh;
+}
+
+body {
+ margin: 0;
+}
+
+#root > .compose-web-column > div {
+ position: relative;
+}
diff --git a/documentation/navigation/multiplatform.md b/documentation/navigation/multiplatform.md
index 2cf2e5d1d..e23501fe1 100644
--- a/documentation/navigation/multiplatform.md
+++ b/documentation/navigation/multiplatform.md
@@ -138,7 +138,7 @@ fun main() = application {
fun main() {
val events: Channel = Channel()
onWasmReady {
- CanvasBasedWindow("Your app") {
+ CanvasBasedWindow("Your app") {
val requester = remember { FocusRequester() }
var hasFocus by remember { mutableStateOf(false) }
var screenSize by remember { mutableStateOf(ScreenSize(0.dp, 0.dp)) }
diff --git a/gradle.properties b/gradle.properties
index 1c621a29a..1bd124c1e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -12,4 +12,5 @@ org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.jetbrains.compose.experimental.jscanvas.enabled=true
+org.jetbrains.compose.experimental.wasm.enabled=true
org.jetbrains.compose.experimental.uikit.enabled=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6093a067b..daf172cc1 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -21,7 +21,7 @@ kotlin = "1.9.23"
ksp = "1.9.23-1.0.19"
mvicore = "1.2.6"
ribs = "0.39.0"
-serialization-json = "1.5.0"
+serialization-json = "1.6.2"
uuid = "9.0.0"
uiautomator = "2.2.0"
benchmark-macro-junit4 = "1.2.0"
diff --git a/plugins/convention/src/main/kotlin/com/bumble/appyx/multiplatform/MultiplatformConventionPlugin.kt b/plugins/convention/src/main/kotlin/com/bumble/appyx/multiplatform/MultiplatformConventionPlugin.kt
index a29a7306f..04f47b8aa 100644
--- a/plugins/convention/src/main/kotlin/com/bumble/appyx/multiplatform/MultiplatformConventionPlugin.kt
+++ b/plugins/convention/src/main/kotlin/com/bumble/appyx/multiplatform/MultiplatformConventionPlugin.kt
@@ -12,6 +12,7 @@ import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl
class MultiplatformConventionPlugin : Plugin {
@@ -34,6 +35,8 @@ class MultiplatformConventionPlugin : Plugin {
"src/desktopTest/kotlin",
"src/jsMain/kotlin",
"src/jsTest/kotlin",
+ "src/wasmJsMain/kotlin",
+ "src/wasmJsTest/kotlin",
"src/iosMain/kotlin",
)
}
diff --git a/utils/customisations/build.gradle.kts b/utils/customisations/build.gradle.kts
index 6112939c7..6270c521d 100644
--- a/utils/customisations/build.gradle.kts
+++ b/utils/customisations/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("com.android.library")
@@ -21,6 +23,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-utils-customisation"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-utils-customisation-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
iosArm64()
@@ -40,4 +58,4 @@ kotlin {
iosSimulatorArm64Main.dependsOn(this)
}
}
-}
+}
\ No newline at end of file
diff --git a/utils/customisations/karma.config.d/wasm/config.js b/utils/customisations/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/utils/customisations/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/utils/customisations/src/wasmJsMain/kotlin/com/bumble/appyx/utils/customisations/NodeCustomisationDirectoryImpl.kt b/utils/customisations/src/wasmJsMain/kotlin/com/bumble/appyx/utils/customisations/NodeCustomisationDirectoryImpl.kt
new file mode 100644
index 000000000..7e369f541
--- /dev/null
+++ b/utils/customisations/src/wasmJsMain/kotlin/com/bumble/appyx/utils/customisations/NodeCustomisationDirectoryImpl.kt
@@ -0,0 +1,33 @@
+package com.bumble.appyx.utils.customisations
+
+import kotlin.reflect.KClass
+
+actual open class NodeCustomisationDirectoryImpl actual constructor(
+ override val parent: NodeCustomisationDirectory?
+) : MutableNodeCustomisationDirectory {
+
+ override fun put(key: KClass, valueProvider: () -> T) {
+ // NO-OP
+ }
+
+ override fun get(key: KClass): T? =
+ null
+
+ override fun getRecursively(key: KClass): T? =
+ null
+
+ override fun putSubDirectory(key: KClass, valueProvider: () -> NodeCustomisationDirectory) {
+ // NO-OP
+ }
+
+ override fun getSubDirectory(key: KClass): NodeCustomisationDirectory? =
+ null
+
+ override fun getSubDirectoryOrSelf(key: KClass): NodeCustomisationDirectory {
+ return this
+ }
+
+ operator fun KClass<*>.invoke(block: NodeCustomisationDirectoryImpl.() -> Unit) {
+ // NO-OP
+ }
+}
diff --git a/utils/material3/build.gradle.kts b/utils/material3/build.gradle.kts
index 2a7d0914d..0b77ea233 100644
--- a/utils/material3/build.gradle.kts
+++ b/utils/material3/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
id("com.android.library")
@@ -22,8 +24,23 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-utils-material3"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-utils-material3-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
-
iosX64()
iosArm64()
iosSimulatorArm64()
@@ -52,3 +69,7 @@ kotlin {
}
}
}
+
+compose.experimental {
+ web.application {}
+}
diff --git a/utils/material3/karma.config.d/wasm/config.js b/utils/material3/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/utils/material3/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/utils/multiplatform/build.gradle.kts b/utils/multiplatform/build.gradle.kts
index eed86cfd3..22938fbd7 100644
--- a/utils/multiplatform/build.gradle.kts
+++ b/utils/multiplatform/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+
plugins {
id("com.bumble.appyx.multiplatform")
kotlin("plugin.serialization")
@@ -23,6 +25,22 @@ kotlin {
// Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
moduleName = "appyx-utils-multiplatform"
browser()
+ binaries.executable()
+ }
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ // Adding moduleName as a workaround for this issue: https://youtrack.jetbrains.com/issue/KT-51942
+ moduleName = "appyx-utils-multiplatform-wa"
+ browser {
+ // Refer to this Slack thread for more details: https://kotlinlang.slack.com/archives/CDFP59223/p1702977410505449?thread_ts=1702668737.674499&cid=CDFP59223
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ useConfigDirectory(project.projectDir.resolve("karma.config.d").resolve("wasm"))
+ }
+ }
+ }
+ binaries.executable()
}
iosX64()
iosArm64()
diff --git a/utils/multiplatform/karma.config.d/wasm/config.js b/utils/multiplatform/karma.config.d/wasm/config.js
new file mode 100644
index 000000000..22429e585
--- /dev/null
+++ b/utils/multiplatform/karma.config.d/wasm/config.js
@@ -0,0 +1,55 @@
+// see https://kotlinlang.org/docs/js-project-setup.html#webpack-configuration-file
+// This file provides karma.config.d configuration to run tests with k/wasm
+
+const path = require("path");
+
+config.browserConsoleLogOptions.level = "debug";
+
+const basePath = config.basePath;
+const projectPath = path.resolve(basePath, "..", "..", "..", "..");
+const generatedAssetsPath = path.resolve(projectPath, "build", "karma-webpack-out")
+
+const debug = message => console.log(`[karma-config] ${message}`);
+
+debug(`karma basePath: ${basePath}`);
+debug(`karma generatedAssetsPath: ${generatedAssetsPath}`);
+
+config.proxies["/"] = path.resolve(basePath, "kotlin");
+
+config.files = [
+ {pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
+ {pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
+].concat(config.files);
+
+function KarmaWebpackOutputFramework(config) {
+ // This controller is instantiated and set during the preprocessor phase.
+ const controller = config.__karmaWebpackController;
+
+ // only if webpack has instantiated its controller
+ if (!controller) {
+ console.warn(
+ "Webpack has not instantiated controller yet.\n" +
+ "Check if you have enabled webpack preprocessor and framework before this framework"
+ )
+ return
+ }
+
+ config.files.push({
+ pattern: `${controller.outputPath}/**/*`,
+ included: false,
+ served: true,
+ watched: false
+ })
+}
+
+const KarmaWebpackOutputPlugin = {
+ 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
+};
+
+config.plugins.push(KarmaWebpackOutputPlugin);
+config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/AppyxLogger.wasmJs.kt b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/AppyxLogger.wasmJs.kt
new file mode 100644
index 000000000..3fa4a92a8
--- /dev/null
+++ b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/AppyxLogger.wasmJs.kt
@@ -0,0 +1,50 @@
+package com.bumble.appyx.utils.multiplatform
+
+import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.DEBUG
+import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.DISABLED
+import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.ERROR
+import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.INFO
+import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.VERBOSE
+import com.bumble.appyx.utils.multiplatform.AppyxLoggingLevel.WARN
+
+external interface Console {
+ fun log(o: String)
+}
+
+external val console: Console
+
+actual object AppyxLogger {
+
+ actual var loggingLevel: Int = DISABLED
+
+ actual fun v(tag: String, message: String) {
+ if (loggingLevel <= VERBOSE) {
+ console.log("$tag: $message")
+ }
+ }
+
+ actual fun d(tag: String, message: String) {
+ if (loggingLevel <= DEBUG) {
+ console.log("$tag: $message")
+ }
+ }
+
+
+ actual fun i(tag: String, message: String) {
+ if (loggingLevel <= INFO) {
+ console.log("$tag: $message")
+ }
+ }
+
+ actual fun w(tag: String, message: String) {
+ if (loggingLevel <= WARN) {
+ console.log("$tag: $message")
+ }
+ }
+
+ actual fun e(tag: String, message: String) {
+ if (loggingLevel <= ERROR) {
+ console.log("$tag: $message")
+ }
+ }
+}
diff --git a/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/BuildFlags.kt b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/BuildFlags.kt
new file mode 100644
index 000000000..133ce0315
--- /dev/null
+++ b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/BuildFlags.kt
@@ -0,0 +1,6 @@
+package com.bumble.appyx.utils.multiplatform
+
+@Suppress("ForbiddenComment")
+actual object BuildFlags {
+ actual val DEBUG: Boolean = false // TODO: provide value from gradle
+}
diff --git a/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/Parcelable.kt b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/Parcelable.kt
new file mode 100644
index 000000000..eef475202
--- /dev/null
+++ b/utils/multiplatform/src/wasmJsMain/kotlin/com/bumble/appyx/utils/multiplatform/Parcelable.kt
@@ -0,0 +1,8 @@
+package com.bumble.appyx.utils.multiplatform
+
+actual annotation class Parcelize
+
+actual interface Parcelable
+
+@Target(AnnotationTarget.TYPE)
+actual annotation class RawValue