├── modules ├── preview-processor │ ├── .gitignore │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── services │ │ │ │ ├── com.google.devtools.ksp.processing.SymbolProcessorProvider │ │ │ │ └── org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar │ │ │ └── kotlin │ │ │ ├── PreviewComponentRegistrar.kt │ │ │ ├── MakePreviewPublicFirExtensionRegistrar.kt │ │ │ └── PreviewProcessor.kt │ └── build.gradle.kts ├── preview-processor-test │ ├── .gitignore │ ├── src │ │ ├── jvmMain │ │ │ └── kotlin │ │ │ │ └── util │ │ │ │ ├── AssertableFile.kt │ │ │ │ └── Compilation.kt │ │ └── androidUnitTest │ │ │ └── kotlin │ │ │ ├── PreviewProcessorAndroidTest.kt │ │ │ └── MakePreviewPublicFirExtensionRegistrarAndroidTest.kt │ └── build.gradle.kts ├── gallery │ ├── src │ │ ├── androidMain │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ └── strings.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ └── drawable-v24 │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin │ │ │ │ └── org │ │ │ │ └── jetbrain │ │ │ │ └── compose │ │ │ │ └── storytale │ │ │ │ └── gallery │ │ │ │ └── material3 │ │ │ │ └── StoryContent.android.kt │ │ ├── commonMain │ │ │ ├── composeResources │ │ │ │ ├── font │ │ │ │ │ └── JetBrainsMono-Regular.woff2 │ │ │ │ └── drawable │ │ │ │ │ ├── info.xml │ │ │ │ │ ├── check.xml │ │ │ │ │ ├── story_widget_icon.xml │ │ │ │ │ ├── arrow_back.xml │ │ │ │ │ ├── copy.xml │ │ │ │ │ ├── wrench.xml │ │ │ │ │ ├── compose-multiplatform.xml │ │ │ │ │ └── palette.xml │ │ │ └── kotlin │ │ │ │ └── org │ │ │ │ └── jetbrains │ │ │ │ └── compose │ │ │ │ └── storytale │ │ │ │ └── gallery │ │ │ │ ├── platform │ │ │ │ └── StoryGallery.kt │ │ │ │ ├── compose │ │ │ │ ├── CompositionLocal.kt │ │ │ │ └── AnnotatedString.kt │ │ │ │ ├── utils │ │ │ │ └── cast.kt │ │ │ │ ├── material3 │ │ │ │ ├── Navigation.kt │ │ │ │ ├── ReponsiveNavigationDrawer.kt │ │ │ │ ├── StorytaleGalleryApp.kt │ │ │ │ └── EmbeddedStoryView.kt │ │ │ │ ├── ui │ │ │ │ ├── theme │ │ │ │ │ └── StoryGalleryTheme.kt │ │ │ │ └── component │ │ │ │ │ ├── CenterRow.kt │ │ │ │ │ └── Gap.kt │ │ │ │ ├── Gallery.kt │ │ │ │ └── story │ │ │ │ └── code │ │ │ │ └── CodeBlock.kt │ │ ├── jvmMain │ │ │ └── kotlin │ │ │ │ └── org │ │ │ │ └── jetbrains │ │ │ │ └── compose │ │ │ │ └── storytale │ │ │ │ └── gallery │ │ │ │ ├── platform │ │ │ │ └── StoryGallery.jvm.kt │ │ │ │ └── material3 │ │ │ │ └── StoryContent.jvm.kt │ │ ├── mobileMain │ │ │ └── kotlin │ │ │ │ └── org │ │ │ │ └── jetbrains │ │ │ │ └── compose │ │ │ │ └── storytale │ │ │ │ └── gallery │ │ │ │ └── platform │ │ │ │ └── StoryGallery.mobile.kt │ │ ├── iosMain │ │ │ └── kotlin │ │ │ │ └── org │ │ │ │ └── jetbrains │ │ │ │ └── compose │ │ │ │ └── storytale │ │ │ │ └── gallery │ │ │ │ └── material3 │ │ │ │ └── StoryContent.ios.kt │ │ ├── jsMain │ │ │ └── kotlin │ │ │ │ └── org │ │ │ │ └── jetbrains │ │ │ │ └── compose │ │ │ │ └── storytale │ │ │ │ └── gallery │ │ │ │ ├── material3 │ │ │ │ └── StoryContent.js.kt │ │ │ │ └── platform │ │ │ │ └── StoryGallery.js.kt │ │ └── wasmJsMain │ │ │ └── kotlin │ │ │ └── org │ │ │ └── jetbrains │ │ │ └── compose │ │ │ └── storytale │ │ │ └── gallery │ │ │ ├── material3 │ │ │ └── StoryContent.wasmJs.kt │ │ │ └── platform │ │ │ └── StoryGallery.wasm.kt │ └── build.gradle.kts ├── gradle-plugin │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── StorytaleXCode.zip │ │ │ └── kotlin │ │ │ └── org │ │ │ └── jetbrains │ │ │ └── compose │ │ │ └── storytale │ │ │ └── plugin │ │ │ ├── WasmMultiplatformTasks.kt │ │ │ ├── NativeSourceGeneratorTask.kt │ │ │ ├── StorytaleExtension.kt │ │ │ ├── JsSourceGeneratorTask.kt │ │ │ ├── NativeCopyResourcesTask.kt │ │ │ ├── JvmSourceGeneratorTask.kt │ │ │ ├── StorytaleGradlePlugin.kt │ │ │ ├── AndroidSourceGeneratorTask.kt │ │ │ ├── WasmSourceGeneratorTask.kt │ │ │ ├── JsMultiplatformTasks.kt │ │ │ ├── Utils.kt │ │ │ └── JvmMultiplatformTasks.kt │ ├── settings.gradle.kts │ └── build.gradle.kts ├── runtime-api │ ├── src │ │ ├── androidMain │ │ │ └── AndroidManifest.xml │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── org │ │ │ └── jetbrains │ │ │ └── compose │ │ │ └── storytale │ │ │ ├── StoryParameter.kt │ │ │ ├── PreviewParameter.kt │ │ │ ├── Story.kt │ │ │ ├── StoryDelegate.kt │ │ │ └── StoryParameterDelegate.kt │ └── build.gradle.kts ├── dokka-plugin │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ └── META-INF │ │ │ │ │ └── services │ │ │ │ │ └── org.jetbrains.dokka.plugability.DokkaPlugin │ │ │ └── kotlin │ │ │ │ └── org │ │ │ │ └── jetbrains │ │ │ │ └── dokka │ │ │ │ └── storytale │ │ │ │ ├── StorytalePlugin.kt │ │ │ │ └── StoryHtmlRenderer.kt │ │ └── test │ │ │ ├── resources │ │ │ └── storytale │ │ │ │ └── module.kt │ │ │ └── kotlin │ │ │ └── org │ │ │ └── jetbrains │ │ │ └── dokka │ │ │ └── storytale │ │ │ └── StorytalePluginTest.kt │ └── build.gradle.kts └── compiler-plugin │ ├── src │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar │ ├── kotlin │ │ └── StorytaleComponentRegistrar.kt │ └── test │ │ └── kotlin │ │ ├── MentionAllStoriesGettersInsideMainFunctionLoweringTest.kt │ │ ├── ReplaceStoryCallWithItsSuccessorWithCodeParameterTest.kt │ │ ├── util │ │ └── StorytaleTest.kt │ │ └── StorytaleComponentRegistrarLearningTest.kt │ └── build.gradle.kts ├── .fleet ├── settings.json └── receipt.json ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── examples ├── iosApp │ ├── Configuration │ │ └── Config.xcconfig │ └── iosApp │ │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── app-icon-1024.png │ │ │ └── Contents.json │ │ └── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ ├── iOSApp.swift │ │ ├── ContentView.swift │ │ └── Info.plist ├── src │ ├── androidMain │ │ ├── res │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── kotlin │ │ │ ├── Platform.android.kt │ │ │ └── org │ │ │ │ └── jetbrains │ │ │ │ └── storytale │ │ │ │ └── example │ │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml │ ├── commonMain │ │ ├── kotlin │ │ │ ├── Platform.kt │ │ │ ├── Greeting.kt │ │ │ ├── ComposeLogo.kt │ │ │ ├── PrimaryButton.kt │ │ │ └── App.kt │ │ └── composeResources │ │ │ └── drawable │ │ │ └── compose-multiplatform.xml │ ├── jsMain │ │ └── kotlin │ │ │ └── Platform.js.kt │ ├── wasmJsMain │ │ └── kotlin │ │ │ └── Platform.wasmJs.kt │ ├── desktopMain │ │ └── kotlin │ │ │ ├── Platform.jvm.kt │ │ │ └── main.kt │ ├── iosMain │ │ └── kotlin │ │ │ └── Platform.ios.kt │ └── commonStories │ │ └── kotlin │ │ ├── ComposeLogo.story.kt │ │ └── PrimaryButton.story.kt ├── .gitignore ├── .fleet │ └── receipt.json └── build.gradle.kts ├── gallery-demo └── src │ ├── wasmJsMain │ ├── kotlin │ │ ├── org │ │ │ └── jetbrains │ │ │ │ └── compose │ │ │ │ └── storytale │ │ │ │ └── generated │ │ │ │ └── MainGenerated.kt │ │ └── storytale │ │ │ └── gallery │ │ │ └── demo │ │ │ └── Main.kt │ └── resources │ │ ├── index.html │ │ └── styles.css │ ├── desktopMain │ └── kotlin │ │ ├── storytale │ │ └── gallery │ │ │ └── demo │ │ │ ├── Main.kt │ │ │ └── PreviewButton.kt │ │ └── org │ │ └── jetbrains │ │ └── compose │ │ └── storytale │ │ └── generated │ │ └── MainGenerated.kt │ └── commonMain │ └── kotlin │ └── storytale │ └── gallery │ └── demo │ ├── Checkbox.story.kt │ ├── PreviewCheckbox.kt │ ├── Simple inputs.story.kt │ ├── Parameters.story.kt │ ├── ColorfulMosaic.story.kt │ └── Buttons.story.kt ├── .github ├── dependabot.yml └── workflows │ ├── spotless.yaml │ ├── smokebuild.yaml │ └── ci.yaml ├── .gitignore ├── gradle.properties ├── .editorconfig ├── settings.gradle.kts ├── gradlew.bat └── README.md /modules/preview-processor/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /modules/preview-processor-test/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /.fleet/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "run.destination.stop.already.running": "Always" 3 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=org.jetbrains.storytale.example 3 | APP_NAME=example -------------------------------------------------------------------------------- /examples/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | example 3 | -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Storytale 3 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /examples/src/commonMain/kotlin/Platform.kt: -------------------------------------------------------------------------------- 1 | interface Platform { 2 | val name: String 3 | } 4 | 5 | expect fun getPlatform(): Platform 6 | -------------------------------------------------------------------------------- /modules/preview-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | PreviewProcessor$Provider 2 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/resources/StorytaleXCode.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gradle-plugin/src/main/resources/StorytaleXCode.zip -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /modules/runtime-api/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/src/jsMain/kotlin/Platform.js.kt: -------------------------------------------------------------------------------- 1 | class JsPlatform : Platform { 2 | override val name: String = "Web with Kotlin/Js" 3 | } 4 | 5 | actual fun getPlatform(): Platform = JsPlatform() 6 | -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /examples/src/commonMain/kotlin/Greeting.kt: -------------------------------------------------------------------------------- 1 | class Greeting { 2 | private val platform = getPlatform() 3 | 4 | fun greet(): String { 5 | return "Hello, ${platform.name}!" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/src/wasmJsMain/kotlin/Platform.wasmJs.kt: -------------------------------------------------------------------------------- 1 | class WasmPlatform : Platform { 2 | override val name: String = "Web with Kotlin/Wasm" 3 | } 4 | 5 | actual fun getPlatform(): Platform = WasmPlatform() 6 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/composeResources/font/JetBrainsMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/commonMain/composeResources/font/JetBrainsMono-Regular.woff2 -------------------------------------------------------------------------------- /examples/src/desktopMain/kotlin/Platform.jvm.kt: -------------------------------------------------------------------------------- 1 | class JVMPlatform : Platform { 2 | override val name: String = "Java ${System.getProperty("java.version")}" 3 | } 4 | 5 | actual fun getPlatform(): Platform = JVMPlatform() 6 | -------------------------------------------------------------------------------- /gallery-demo/src/wasmJsMain/kotlin/org/jetbrains/compose/storytale/generated/MainGenerated.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.generated 2 | 3 | @Suppress("ktlint:standard:function-naming") 4 | fun MainViewController() {} 5 | -------------------------------------------------------------------------------- /gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/Main.kt: -------------------------------------------------------------------------------- 1 | package storytale.gallery.demo 2 | 3 | import org.jetbrains.compose.storytale.generated.MainViewController 4 | 5 | fun main() { 6 | MainViewController() 7 | } 8 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /examples/src/androidMain/kotlin/Platform.android.kt: -------------------------------------------------------------------------------- 1 | import android.os.Build 2 | 3 | class AndroidPlatform : Platform { 4 | override val name: String = "Android ${Build.VERSION.SDK_INT}" 5 | } 6 | 7 | actual fun getPlatform(): Platform = AndroidPlatform() 8 | -------------------------------------------------------------------------------- /modules/dokka-plugin/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2014-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | # 4 | 5 | org.jetbrains.dokka.storytale.StorytalePlugin 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.platform 2 | 3 | import androidx.navigation.NavHostController 4 | 5 | expect suspend fun bindNavigation(navController: NavHostController) 6 | -------------------------------------------------------------------------------- /examples/src/iosMain/kotlin/Platform.ios.kt: -------------------------------------------------------------------------------- 1 | import platform.UIKit.UIDevice 2 | 3 | class IOSPlatform : Platform { 4 | override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion 5 | } 6 | 7 | actual fun getPlatform(): Platform = IOSPlatform() 8 | -------------------------------------------------------------------------------- /modules/gallery/src/jvmMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.jvm.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.platform 2 | 3 | import androidx.navigation.NavHostController 4 | 5 | actual suspend fun bindNavigation(navController: NavHostController) { 6 | } 7 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/compose/CompositionLocal.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.compose 2 | 3 | inline fun noCompositionLocalProvided(): T { 4 | error("CompositionLocal ${T::class.simpleName} not present") 5 | } 6 | -------------------------------------------------------------------------------- /modules/gallery/src/mobileMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.mobile.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.platform 2 | 3 | import androidx.navigation.NavHostController 4 | 5 | actual suspend fun bindNavigation(navController: NavHostController) { 6 | } 7 | -------------------------------------------------------------------------------- /examples/src/desktopMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.Window 2 | import androidx.compose.ui.window.application 3 | 4 | fun main() = application { 5 | Window( 6 | onCloseRequest = ::exitApplication, 7 | title = "example", 8 | ) { 9 | App() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/gradle-plugin/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | versionCatalogs { 7 | create("libs") { 8 | from(files("../../gradle/libs.versions.toml")) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/utils/cast.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.utils 2 | 3 | @Throws(ClassCastException::class) 4 | internal inline fun Any?.cast(): T = this as T 5 | 6 | internal inline fun Any?.castOrNull(): T? = this as? T 7 | -------------------------------------------------------------------------------- /modules/preview-processor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) 3 | } 4 | 5 | dependencies { 6 | implementation(libs.kotlin.poet) 7 | implementation(libs.ksp.api) 8 | implementation(kotlin("compiler-embeddable")) 9 | implementation("org.jetbrains.compose.storytale:gradle-plugin") 10 | } 11 | -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 08 17:33:08 CST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /examples/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/Navigation.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.material3 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | @SerialName("story") 8 | data class StoryScreen(val storyName: String) 9 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /modules/dokka-plugin/src/test/resources/storytale/module.kt: -------------------------------------------------------------------------------- 1 | package storytale 2 | 3 | /** 4 | * Test function with story annotation 5 | * @story https://storytale.io/stories/12345 6 | */ 7 | fun testWithStory(): String = "Story function" 8 | 9 | /** 10 | * Regular function without story 11 | */ 12 | fun testWithoutStory(): String = "Regular function" 13 | -------------------------------------------------------------------------------- /modules/gallery/src/iosMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StoryContent.ios.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.material3 2 | 3 | import androidx.compose.ui.platform.ClipEntry 4 | import androidx.compose.ui.platform.Clipboard 5 | 6 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) { 7 | this.setClipEntry(ClipEntry.withPlainText(code)) 8 | } 9 | -------------------------------------------------------------------------------- /modules/gallery/src/jsMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StoryContent.js.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.material3 2 | 3 | import androidx.compose.ui.platform.ClipEntry 4 | import androidx.compose.ui.platform.Clipboard 5 | 6 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) { 7 | this.setClipEntry(ClipEntry.withPlainText(code)) 8 | } 9 | -------------------------------------------------------------------------------- /modules/gallery/src/wasmJsMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StoryContent.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.material3 2 | 3 | import androidx.compose.ui.platform.ClipEntry 4 | import androidx.compose.ui.platform.Clipboard 5 | 6 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) { 7 | this.setClipEntry(ClipEntry.withPlainText(code)) 8 | } 9 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .kotlin 3 | .gradle 4 | **/build/ 5 | xcuserdata 6 | !src/**/build/ 7 | local.properties 8 | .idea 9 | .DS_Store 10 | captures 11 | .externalNativeBuild 12 | .cxx 13 | *.xcodeproj/* 14 | !*.xcodeproj/project.pbxproj 15 | !*.xcodeproj/xcshareddata/ 16 | !*.xcodeproj/project.xcworkspace/ 17 | !*.xcworkspace/contents.xcworkspacedata 18 | **/xcshareddata/WorkspaceSettings.xcsettings 19 | -------------------------------------------------------------------------------- /modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryParameter.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale 2 | 3 | import androidx.compose.runtime.MutableState 4 | import kotlin.reflect.KClass 5 | 6 | class StoryParameter( 7 | val name: String, 8 | val type: KClass<*>, 9 | val values: List<*>?, 10 | val label: String?, 11 | val state: MutableState, 12 | ) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | **/build/ 4 | xcuserdata 5 | !src/**/build/ 6 | local.properties 7 | .idea 8 | .DS_Store 9 | captures 10 | .kotlin 11 | .externalNativeBuild 12 | .cxx 13 | /example/build 14 | *.xcodeproj/* 15 | !*.xcodeproj/project.pbxproj 16 | !*.xcodeproj/xcshareddata/ 17 | !*.xcodeproj/project.xcworkspace/ 18 | !*.xcworkspace/contents.xcworkspacedata 19 | **/xcshareddata/WorkspaceSettings.xcsettings 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | #Gradle 4 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 5 | 6 | #Android 7 | android.nonTransitiveRClass=true 8 | android.useAndroidX=true 9 | 10 | #Compose 11 | org.jetbrains.compose.experimental.jscanvas.enabled=true 12 | 13 | kotlin.jvm.target.validation.mode=ignore 14 | 15 | #Publication 16 | storytale.deploy.version=0.0.4-SNAPSHOT 17 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/compose/AnnotatedString.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.compose 2 | 3 | import androidx.compose.ui.text.AnnotatedString.Builder 4 | import androidx.compose.ui.text.SpanStyle 5 | import androidx.compose.ui.text.withStyle 6 | 7 | fun Builder.text(text: String, style: SpanStyle = SpanStyle()) = withStyle(style = style) { 8 | append(text) 9 | } 10 | -------------------------------------------------------------------------------- /gallery-demo/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Storytale Gallery Demo 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /modules/gallery/src/jvmMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StoryContent.jvm.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.material3 2 | 3 | import androidx.compose.ui.platform.ClipEntry 4 | import androidx.compose.ui.platform.Clipboard 5 | import java.awt.datatransfer.StringSelection 6 | 7 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) { 8 | setClipEntry(ClipEntry(StringSelection(code))) 9 | } 10 | -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/kotlin/org/jetbrain/compose/storytale/gallery/material3/StoryContent.android.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.material3 2 | 3 | import android.content.ClipData 4 | import androidx.compose.ui.platform.ClipEntry 5 | import androidx.compose.ui.platform.Clipboard 6 | 7 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) { 8 | val clipData = ClipData.newPlainText("Copied Code", code) 9 | setClipEntry(ClipEntry(clipData)) 10 | } 11 | -------------------------------------------------------------------------------- /modules/gallery/src/jsMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.js.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.platform 2 | 3 | import androidx.navigation.ExperimentalBrowserHistoryApi 4 | import androidx.navigation.NavHostController 5 | import androidx.navigation.bindToNavigation 6 | import kotlinx.browser.window 7 | 8 | @OptIn(ExperimentalBrowserHistoryApi::class) 9 | actual suspend fun bindNavigation(navController: NavHostController) { 10 | window.bindToNavigation(navController) 11 | } 12 | -------------------------------------------------------------------------------- /modules/gallery/src/wasmJsMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.wasm.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.platform 2 | 3 | import androidx.navigation.ExperimentalBrowserHistoryApi 4 | import androidx.navigation.NavHostController 5 | import androidx.navigation.bindToNavigation 6 | import kotlinx.browser.window 7 | 8 | actual suspend fun bindNavigation(navController: NavHostController) { 9 | @OptIn(ExperimentalBrowserHistoryApi::class) 10 | window.bindToNavigation(navController) 11 | } 12 | -------------------------------------------------------------------------------- /examples/src/commonStories/kotlin/ComposeLogo.story.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.size 2 | import androidx.compose.ui.Modifier 3 | import androidx.compose.ui.unit.dp 4 | import org.jetbrains.compose.storytale.story 5 | 6 | val `ComposeLogo Default State` by story { 7 | ComposeLogo() 8 | } 9 | 10 | val `ComposeLogo Small` by story { 11 | ComposeLogo(Modifier.size(300.dp)) 12 | } 13 | 14 | val `ComposeLogo Medium` by story { 15 | ComposeLogo(Modifier.size(600.dp)) 16 | } 17 | 18 | val `ComposeLogo Large` by story { 19 | ComposeLogo(Modifier.size(900.dp)) 20 | } 21 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/src/commonMain/kotlin/ComposeLogo.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.Image 2 | import androidx.compose.runtime.Composable 3 | import androidx.compose.ui.Modifier 4 | import org.jetbrains.compose.resources.painterResource 5 | import org.jetbrains.compose.storytale.example.Res 6 | import org.jetbrains.compose.storytale.example.compose_multiplatform 7 | 8 | @Composable 9 | fun ComposeLogo( 10 | modifier: Modifier = Modifier, 11 | ) { 12 | Image( 13 | painter = painterResource(Res.drawable.compose_multiplatform), 14 | contentDescription = "Compose Multiplatform Logo", 15 | modifier = modifier, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/spotless.yaml: -------------------------------------------------------------------------------- 1 | name: Spotless Check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | spotless: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up JDK 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x ./gradlew 25 | 26 | - name: Run Spotless Check 27 | run: ./gradlew spotlessCheck 28 | -------------------------------------------------------------------------------- /examples/src/androidMain/kotlin/org/jetbrains/storytale/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.storytale.example 2 | 3 | import App 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.tooling.preview.Preview 9 | 10 | class MainActivity : ComponentActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | setContent { 15 | App() 16 | } 17 | } 18 | } 19 | 20 | @Preview 21 | @Composable 22 | private fun AppAndroidPreview() { 23 | App() 24 | } 25 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/composeResources/drawable/info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 14 | 17 | -------------------------------------------------------------------------------- /modules/preview-processor/src/main/kotlin/PreviewComponentRegistrar.kt: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 2 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 3 | import org.jetbrains.kotlin.config.CompilerConfiguration 4 | import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter 5 | 6 | @OptIn(ExperimentalCompilerApi::class) 7 | class PreviewComponentRegistrar : CompilerPluginRegistrar() { 8 | override val supportsK2: Boolean get() = true 9 | 10 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { 11 | FirExtensionRegistrarAdapter.registerExtension(MakePreviewPublicFirExtensionRegistrar()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /gallery-demo/src/desktopMain/kotlin/org/jetbrains/compose/storytale/generated/MainGenerated.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.generated 2 | 3 | import androidx.compose.ui.unit.dp 4 | import androidx.compose.ui.window.WindowState 5 | import androidx.compose.ui.window.singleWindowApplication 6 | import org.jetbrains.compose.storytale.gallery.material3.StorytaleGalleryApp 7 | 8 | // To let the Storytale compiler plugin add the initializations for stories 9 | @Suppress("ktlint:standard:function-naming") 10 | fun MainViewController() { 11 | singleWindowApplication( 12 | state = WindowState(width = 800.dp, height = 800.dp), 13 | alwaysOnTop = true, 14 | ) { 15 | StorytaleGalleryApp() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.fleet/receipt.json: -------------------------------------------------------------------------------- 1 | // Project generated by Kotlin Multiplatform Wizard 2 | { 3 | "spec": { 4 | "template_id": "kmt", 5 | "targets": { 6 | "android": { 7 | "ui": [ 8 | "compose" 9 | ] 10 | }, 11 | "ios": { 12 | "ui": [ 13 | "compose" 14 | ] 15 | }, 16 | "desktop": { 17 | "ui": [ 18 | "compose" 19 | ] 20 | }, 21 | "web": { 22 | "ui": [ 23 | "compose" 24 | ] 25 | } 26 | } 27 | }, 28 | "timestamp": "2024-05-08T16:38:45.656209060Z" 29 | } -------------------------------------------------------------------------------- /examples/.fleet/receipt.json: -------------------------------------------------------------------------------- 1 | // Project generated by Kotlin Multiplatform Wizard 2 | { 3 | "spec": { 4 | "template_id": "kmt", 5 | "targets": { 6 | "android": { 7 | "ui": [ 8 | "compose" 9 | ] 10 | }, 11 | "ios": { 12 | "ui": [ 13 | "compose" 14 | ] 15 | }, 16 | "desktop": { 17 | "ui": [ 18 | "compose" 19 | ] 20 | }, 21 | "web": { 22 | "ui": [ 23 | "compose" 24 | ] 25 | } 26 | } 27 | }, 28 | "timestamp": "2024-05-26T06:06:11.223920641Z" 29 | } -------------------------------------------------------------------------------- /modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2010-2023 JetBrains s.r.o. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | PreviewComponentRegistrar 18 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/composeResources/drawable/check.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/Checkbox.story.kt: -------------------------------------------------------------------------------- 1 | package storytale.gallery.demo 2 | 3 | import androidx.compose.material3.TriStateCheckbox 4 | import androidx.compose.ui.state.ToggleableState 5 | import androidx.compose.ui.state.ToggleableState.Indeterminate 6 | import androidx.compose.ui.state.ToggleableState.Off 7 | import androidx.compose.ui.state.ToggleableState.On 8 | import org.jetbrains.compose.storytale.story 9 | 10 | val `Check box` by story { 11 | var state by parameter(ToggleableState.entries) 12 | 13 | TriStateCheckbox( 14 | state = state, 15 | onClick = { 16 | state = when (state) { 17 | On -> Indeterminate 18 | Off -> On 19 | Indeterminate -> Off 20 | } 21 | }, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /modules/compiler-plugin/src/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2010-2023 JetBrains s.r.o. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | org.jetbrains.compose.plugin.storytale.compiler.StorytaleComponentRegistrar 18 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/ui/theme/StoryGalleryTheme.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.ui.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.compose.runtime.staticCompositionLocalOf 6 | import androidx.compose.ui.platform.LocalDensity 7 | import androidx.compose.ui.unit.Density 8 | import org.jetbrains.compose.storytale.gallery.compose.noCompositionLocalProvided 9 | 10 | val LocalCustomDensity = staticCompositionLocalOf { noCompositionLocalProvided() } 11 | 12 | @Composable 13 | inline fun UseCustomDensity(crossinline content: @Composable () -> Unit) { 14 | CompositionLocalProvider(LocalDensity provides LocalCustomDensity.current) { 15 | content() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/composeResources/drawable/story_widget_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/ui/component/CenterRow.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.ui.component 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.RowScope 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | inline fun CenterRow( 12 | modifier: Modifier = Modifier, 13 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, 14 | content: @Composable RowScope.() -> Unit, 15 | ) = Row( 16 | verticalAlignment = Alignment.CenterVertically, 17 | horizontalArrangement = horizontalArrangement, 18 | modifier = modifier, 19 | ) { 20 | content() 21 | } 22 | -------------------------------------------------------------------------------- /modules/compiler-plugin/src/kotlin/StorytaleComponentRegistrar.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.plugin.storytale.compiler 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 4 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 5 | import org.jetbrains.kotlin.config.CompilerConfiguration 6 | 7 | class StorytaleComponentRegistrar : CompilerPluginRegistrar() { 8 | override val supportsK2: Boolean get() = true 9 | 10 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { 11 | Companion.registerExtensions(this) 12 | } 13 | 14 | companion object { 15 | fun registerExtensions(extensionStorage: ExtensionStorage) = with(extensionStorage) { 16 | IrGenerationExtension.registerExtension(StorytaleLoweringExtension()) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.staticCompositionLocalOf 5 | 6 | @Composable 7 | inline fun previewParameter(defaultValue: T) = LocalStory.current.parameter(defaultValue) 8 | 9 | @Composable 10 | inline fun previewParameter( 11 | values: List, 12 | defaultValueIndex: Int = 0, 13 | label: String? = null, 14 | ) = LocalStory.current.parameter(values, defaultValueIndex, label) 15 | 16 | @Composable 17 | inline fun > previewParameter(defaultValue: T, label: String? = null) = LocalStory.current.parameter(defaultValue, label) 18 | 19 | @PublishedApi 20 | internal val LocalStory = staticCompositionLocalOf { Story(-1, "DefaultPreviewStory", "", "", {}) } 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 4 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{kt,kts}] 11 | ij_kotlin_imports_layout = * 12 | ij_kotlin_allow_trailing_comma = true 13 | ij_kotlin_allow_trailing_comma_on_call_site = true 14 | ij_kotlin_line_break_after_multiline_when_entry = false 15 | ij_kotlin_name_count_to_use_star_import = 2147483647 16 | ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 17 | ij_kotlin_packages_to_use_import_on_demand = unset 18 | ktlint_code_style = intellij_idea 19 | ktlint_function_naming_ignore_when_annotated_with = Composable 20 | ktlint_standard_function-expression-body = disabled 21 | ktlint_standard_filename = disabled 22 | ktlint_compose_modifier-missing-check = disabled 23 | 24 | [*.md] 25 | trim_trailing_whitespace = false 26 | 27 | [*.xml] 28 | indent_size = 4 -------------------------------------------------------------------------------- /modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import java.io.File 4 | import org.assertj.core.util.Files 5 | import org.intellij.lang.annotations.Language 6 | 7 | data class AssertableFile( 8 | val path: String, 9 | val content: String, 10 | ) { 11 | override fun toString(): String { 12 | return buildString { 13 | append('\"') 14 | append(path) 15 | appendLine("\" has content \"\"\"") 16 | appendLine(content) 17 | appendLine("\"\"\"") 18 | } 19 | } 20 | } 21 | 22 | fun File.assertable(): AssertableFile { 23 | val it = this 24 | return path hasContent Files.contentOf(it, Charsets.UTF_8).trim() 25 | } 26 | 27 | @Suppress("NOTHING_TO_INLINE") 28 | inline infix fun String.hasContent(@Language("kotlin") content: String): AssertableFile { 29 | return AssertableFile(this, content) 30 | } 31 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/Gallery.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.navigation.compose.rememberNavController 6 | import org.jetbrains.compose.storytale.gallery.material3.StorytaleGalleryApp 7 | import org.jetbrains.compose.storytale.gallery.platform.bindNavigation 8 | 9 | @Suppress("unused") // The call of this function is added implicitly in the generated code 10 | @Composable 11 | fun Gallery() { 12 | StorytaleGalleryApp() 13 | } 14 | 15 | @Composable 16 | fun Gallery(isEmbedded: Boolean) { 17 | val navHostController = rememberNavController() 18 | 19 | StorytaleGalleryApp(isEmbedded = isEmbedded, navHostController = navHostController) 20 | 21 | LaunchedEffect(Unit) { 22 | bindNavigation(navHostController) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/composeResources/drawable/arrow_back.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt: -------------------------------------------------------------------------------- 1 | package storytale.gallery.demo 2 | 3 | import androidx.compose.material3.TriStateCheckbox 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.state.ToggleableState 6 | import androidx.compose.ui.state.ToggleableState.Indeterminate 7 | import androidx.compose.ui.state.ToggleableState.Off 8 | import androidx.compose.ui.state.ToggleableState.On 9 | import org.jetbrains.compose.storytale.previewParameter 10 | import org.jetbrains.compose.ui.tooling.preview.Preview 11 | 12 | @Preview 13 | @Composable 14 | @Suppress("ktlint") 15 | internal fun PreviewCheckbox() { 16 | var state by previewParameter(ToggleableState.entries) 17 | 18 | TriStateCheckbox( 19 | state = state, 20 | onClick = { 21 | state = when (state) { 22 | On -> Indeterminate 23 | Off -> On 24 | Indeterminate -> Off 25 | } 26 | }, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /examples/src/commonMain/kotlin/PrimaryButton.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.material.Button 2 | import androidx.compose.material.Text 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.unit.sp 6 | 7 | @Composable 8 | fun PrimaryButton( 9 | text: String, 10 | modifier: Modifier = Modifier, 11 | onClick: () -> Unit = {}, 12 | enabled: Boolean = true, 13 | size: PrimaryButtonSize = PrimaryButtonSize.Medium, 14 | ) { 15 | Button( 16 | modifier = modifier, 17 | onClick = onClick, 18 | enabled = enabled, 19 | ) { 20 | Text( 21 | text = text, 22 | fontSize = when (size) { 23 | PrimaryButtonSize.Small -> 12.sp 24 | PrimaryButtonSize.Medium -> 14.sp 25 | PrimaryButtonSize.Large -> 20.sp 26 | }, 27 | ) 28 | } 29 | } 30 | 31 | enum class PrimaryButtonSize { 32 | Small, 33 | Medium, 34 | Large, 35 | } 36 | -------------------------------------------------------------------------------- /examples/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/src/commonStories/kotlin/PrimaryButton.story.kt: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.storytale.story 2 | 3 | val PrimaryButton by story { 4 | val size by parameter(PrimaryButtonSize.Medium) 5 | val enabled by parameter(true) 6 | val text by parameter("Click Me") 7 | 8 | PrimaryButton(text = text, enabled = enabled, size = size) 9 | } 10 | 11 | val `PrimaryButton Default State` by story { 12 | PrimaryButton("Click Me") 13 | } 14 | 15 | val `PrimaryButton Disabled` by story { 16 | PrimaryButton( 17 | text = "Click Me", 18 | enabled = false, 19 | ) 20 | } 21 | 22 | val `PrimaryButton Food` by story { 23 | val foodEmojiList = listOf( 24 | "Apple 🍎", 25 | "Banana 🍌", 26 | "Cherry 🍒", 27 | "Grapes 🍇", 28 | "Strawberry 🍓", 29 | "Watermelon 🍉", 30 | "Pineapple 🍍", 31 | "Pizza 🍕", 32 | "Burger 🍔", 33 | "Fries 🍟", 34 | "Ice Cream 🍦", 35 | "Cake 🍰", 36 | "Coffee ☕", 37 | "Beer 🍺", 38 | ) 39 | 40 | val food by parameter(foodEmojiList) 41 | PrimaryButton(text = food) 42 | } 43 | -------------------------------------------------------------------------------- /gallery-demo/src/wasmJsMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | html, body { 19 | width: 100%; 20 | height: 100%; 21 | margin: 0; 22 | padding: 0; 23 | overflow: hidden; 24 | } 25 | 26 | h1 { 27 | padding: 0 16px; 28 | font-size: 2em; 29 | } 30 | 31 | #composeApplication { 32 | width: 100%; 33 | height: 100%; 34 | } 35 | 36 | body:has(textarea) { 37 | background-color: aliceblue; 38 | } 39 | 40 | body:has(textarea:focus) { 41 | background-color: #eaffe3; 42 | } -------------------------------------------------------------------------------- /modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/Story.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlin.enums.enumEntries 5 | 6 | data class Story( 7 | val id: Int, 8 | val name: String, 9 | val group: String, 10 | val code: String, 11 | val content: @Composable Story.() -> Unit, 12 | ) { 13 | @PublishedApi 14 | internal val nameToParameterMapping = linkedMapOf>() // using linkedMap to keep the order 15 | val parameters inline get() = nameToParameterMapping.values.toList() 16 | 17 | inline fun parameter(defaultValue: T) = StoryParameterDelegate(this, T::class, defaultValue) 18 | 19 | inline fun parameter(values: List, defaultValueIndex: Int = 0, label: String? = null) = StoryListParameterDelegate(this, T::class, values, defaultValueIndex, label) 20 | 21 | inline fun > parameter(defaultValue: T, label: String? = null): StoryListParameterDelegate { 22 | val enumEntries = enumEntries() 23 | return StoryListParameterDelegate(this, T::class, enumEntries, enumEntries.indexOf(defaultValue), label) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/smokebuild.yaml: -------------------------------------------------------------------------------- 1 | name: Smoke build 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - "main" 9 | 10 | jobs: 11 | build: 12 | name: Publish to MavenLocal and Build Examples 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Set up JDK 16 | uses: actions/setup-java@v2 17 | with: 18 | distribution: 'adopt' 19 | java-version: '17' 20 | 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | 24 | - name: Test Dokka Plugin 25 | run: ./gradlew :modules:dokka-plugin:test 26 | 27 | - name: Publish to Maven Local 28 | run: ./gradlew publishToMavenLocal 29 | 30 | - name: Build gallery-demo 31 | run: | 32 | ./gradlew :gallery-demo:wasmJsBrowserDevelopmentExecutableDistribution :gallery-demo:packageReleaseUberJarForCurrentOS 33 | 34 | - name: Build Stories for Wasm target 35 | run: | 36 | cd examples 37 | ../gradlew wasmJsBrowserStoriesProductionExecutableDistribution 38 | 39 | - name: Build Stories for Desktop target 40 | run: | 41 | cd examples 42 | ../gradlew desktopStorytaleGenerate 43 | -------------------------------------------------------------------------------- /gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/Simple inputs.story.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("ktlint:standard:property-naming") 2 | 3 | package storytale.gallery.demo 4 | 5 | import androidx.compose.material3.Button 6 | import androidx.compose.material3.ButtonDefaults 7 | import androidx.compose.material3.Checkbox 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Switch 10 | import androidx.compose.material3.Text 11 | import org.jetbrains.compose.storytale.story 12 | 13 | val Button by story { 14 | val Label by parameter("Click Me") 15 | val Enabled by parameter(true) 16 | val bgColorAlpha by parameter(1f) 17 | 18 | Button( 19 | enabled = Enabled, 20 | onClick = {}, 21 | colors = ButtonDefaults.buttonColors().copy( 22 | containerColor = MaterialTheme.colorScheme.primary.copy(alpha = bgColorAlpha), 23 | ), 24 | ) { 25 | Text(Label) 26 | } 27 | } 28 | 29 | val Checkbox by story { 30 | var checked by parameter(false) 31 | Checkbox(checked, onCheckedChange = { checked = it }) 32 | } 33 | 34 | val Switch by story { 35 | var checked by parameter(false) 36 | Switch(checked, onCheckedChange = { checked = it }) 37 | } 38 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/composeResources/drawable/copy.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 23 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/WasmMultiplatformTasks.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import org.gradle.api.Project 4 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget 5 | 6 | fun Project.processWasmCompilation(extension: StorytaleExtension, target: KotlinJsIrTarget) { 7 | project.logger.info("Configuring storytale using Kotlin/Wasm") 8 | createWasmStorytaleGenerateSourceTask(extension, target) 9 | 10 | val storytaleCompilation = createWasmAndJsStorytaleCompilation(extension, target) 11 | 12 | createWasmAndJsStorytaleExecTask(storytaleCompilation) 13 | } 14 | 15 | private fun Project.createWasmStorytaleGenerateSourceTask(extension: StorytaleExtension, target: KotlinJsIrTarget) { 16 | val storytaleBuildDir = extension.getBuildDirectory(target) 17 | tasks.register("${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}", WasmSourceGeneratorTask::class.java) { 18 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP 19 | description = "Generate Wasm source files for '${target.name}'" 20 | title = target.name 21 | outputSourcesDir = file("$storytaleBuildDir/sources") 22 | outputResourcesDir = file("$storytaleBuildDir/resources") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/dokka-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | `maven-publish` 7 | alias(libs.plugins.kotlin.jvm) 8 | alias(libs.plugins.dokka) 9 | } 10 | 11 | group = "org.jetbrains.compose.storytale" 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | compileOnly(libs.dokka.core) 19 | implementation(libs.dokka.base) 20 | implementation(libs.kotlinx.html) 21 | 22 | testImplementation(libs.jsoup) 23 | testImplementation(libs.dokka.test.api) 24 | testImplementation(libs.dokka.base.test.utils) 25 | testImplementation(libs.dokka.base) 26 | testImplementation(libs.junit.api) 27 | testImplementation(libs.kotlin.test) 28 | } 29 | 30 | tasks.withType { 31 | useJUnitPlatform() 32 | } 33 | 34 | kotlin { 35 | jvmToolchain(11) 36 | } 37 | 38 | val emptyJavadocJar by tasks.registering(Jar::class) { 39 | archiveClassifier.set("javadoc") 40 | } 41 | 42 | publishing { 43 | publications { 44 | create("maven") { 45 | artifactId = "storytale-dokka-plugin" 46 | from(components["kotlin"]) 47 | } 48 | withType { 49 | artifact(emptyJavadocJar) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /modules/runtime-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | 4 | plugins { 5 | alias(libs.plugins.androidLibrary) 6 | alias(libs.plugins.kotlinMultiplatform) 7 | alias(libs.plugins.jetbrainsCompose) 8 | alias(libs.plugins.compose.compiler) 9 | `maven-publish` 10 | } 11 | 12 | kotlin { 13 | wasmJs { 14 | browser() 15 | } 16 | js { 17 | browser() 18 | } 19 | iosX64() 20 | iosArm64() 21 | iosSimulatorArm64() 22 | jvm { 23 | compilerOptions { 24 | jvmTarget.set(JvmTarget.JVM_11) 25 | } 26 | } 27 | androidTarget { 28 | publishLibraryVariants("release") 29 | 30 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 31 | compilerOptions { 32 | jvmTarget.set(JvmTarget.JVM_11) 33 | } 34 | } 35 | 36 | sourceSets { 37 | commonMain.dependencies { 38 | implementation(compose.runtime) 39 | } 40 | } 41 | } 42 | 43 | group = "org.jetbrains.compose.storytale" 44 | 45 | publishing {} 46 | 47 | android { 48 | namespace = "org.jetbrains.compose.storytale.runtime" 49 | compileSdk = libs.versions.android.compileSdk.get().toInt() 50 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 51 | } 52 | -------------------------------------------------------------------------------- /modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryDelegate.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import kotlin.reflect.KProperty 6 | 7 | val storiesStorage = mutableListOf() 8 | 9 | inline fun story( 10 | code: String = "", 11 | group: String = "", 12 | crossinline content: @Composable Story.() -> Unit, 13 | ) = StoryDelegate({ content() }, group, code) 14 | 15 | class StoryDelegate( 16 | private val content: @Composable Story.() -> Unit, 17 | private val group: String = "", 18 | private val code: String = "", 19 | ) { 20 | private lateinit var instance: Story 21 | 22 | operator fun getValue(thisRef: Any?, property: KProperty<*>): Story { 23 | return instance 24 | } 25 | 26 | operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): StoryDelegate { 27 | val wrappedContent: @Composable Story.() -> Unit = { 28 | val story = this 29 | val delegate = this@StoryDelegate 30 | 31 | CompositionLocalProvider(LocalStory provides story) { 32 | delegate.content(story) 33 | } 34 | } 35 | 36 | instance = Story(storiesStorage.size, property.name, group, code, wrappedContent) 37 | .also(storiesStorage::add) 38 | return this 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/src/commonMain/kotlin/App.kt: -------------------------------------------------------------------------------- 1 | 2 | import androidx.compose.animation.AnimatedVisibility 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun App(modifier: Modifier = Modifier) { 19 | var showContent by remember { mutableStateOf(false) } 20 | 21 | MaterialTheme { 22 | Column( 23 | modifier = modifier.fillMaxWidth(), 24 | horizontalAlignment = Alignment.CenterHorizontally, 25 | ) { 26 | PrimaryButton(text = "Click Me", onClick = { showContent = !showContent }) 27 | AnimatedVisibility(showContent) { 28 | val greeting = remember { Greeting().greet() } 29 | Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { 30 | ComposeLogo(Modifier.size(600.dp)) 31 | Text("Compose: $greeting") 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/Parameters.story.kt: -------------------------------------------------------------------------------- 1 | package storytale.gallery.demo 2 | 3 | import androidx.compose.foundation.layout.size 4 | import androidx.compose.material3.Button 5 | import androidx.compose.material3.Text 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.unit.dp 8 | import org.jetbrains.compose.storytale.story 9 | 10 | val foodEmojiList = listOf( 11 | "Apple 🍎", 12 | "Banana 🍌", 13 | "Cherry 🍒", 14 | "Grapes 🍇", 15 | "Strawberry 🍓", 16 | "Watermelon 🍉", 17 | "Pineapple 🍍", 18 | "Pizza 🍕", 19 | "Burger 🍔", 20 | "Fries 🍟", 21 | "Ice Cream 🍦", 22 | "Cake 🍰", 23 | "Coffee ☕", 24 | "Beer 🍺", 25 | ) 26 | 27 | val `List Parameters` by story { 28 | val food by parameter(foodEmojiList) 29 | 30 | Button(onClick = {}) { 31 | Text(food) 32 | } 33 | } 34 | 35 | enum class PrimaryButtonSize { 36 | XS, 37 | Small, 38 | Medium, 39 | Large, 40 | XL, 41 | XXL, 42 | } 43 | 44 | val `Enum Parameters` by story { 45 | val size by parameter(PrimaryButtonSize.Medium, label = null) 46 | 47 | Button( 48 | onClick = {}, 49 | modifier = Modifier.size( 50 | when (size) { 51 | PrimaryButtonSize.XS -> 60.dp 52 | PrimaryButtonSize.Small -> 90.dp 53 | PrimaryButtonSize.Medium -> 120.dp 54 | PrimaryButtonSize.Large -> 150.dp 55 | PrimaryButtonSize.XL -> 170.dp 56 | PrimaryButtonSize.XXL -> 200.dp 57 | }, 58 | ), 59 | ) { 60 | Text(size.toString()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/ColorfulMosaic.story.kt: -------------------------------------------------------------------------------- 1 | package storytale.gallery.demo 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.lazy.grid.GridCells 9 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 10 | import androidx.compose.foundation.lazy.grid.items 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.unit.dp 16 | import org.jetbrains.compose.storytale.story 17 | 18 | val ColorfulMosaic by story { 19 | val colors = listOf( 20 | Color(0xFFE57373), Color(0xFFF06292), Color(0xFFBA68C8), Color(0xFF9575CD), 21 | Color(0xFF7986CB), Color(0xFF64B5F6), Color(0xFF4FC3F7), Color(0xFF4DD0E1), 22 | Color(0xFF4DB6AC), Color(0xFF81C784), Color(0xFFAED581), Color(0xFFFFD54F), 23 | Color(0xFFFFB74D), Color(0xFFFF8A65), Color(0xFFA1887F), Color(0xFF90A4AE), 24 | ) 25 | 26 | LazyVerticalGrid( 27 | columns = GridCells.Adaptive(120.dp), 28 | modifier = Modifier.fillMaxSize(), 29 | ) { 30 | items(colors) { color -> 31 | Box( 32 | modifier = Modifier 33 | .padding(8.dp) 34 | .aspectRatio(1f) 35 | .clip(RoundedCornerShape(8.dp)) 36 | .background(color), 37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /modules/compiler-plugin/src/test/kotlin/MentionAllStoriesGettersInsideMainFunctionLoweringTest.kt: -------------------------------------------------------------------------------- 1 | import com.tschuchort.compiletesting.KotlinCompilation 2 | import kotlin.test.Test 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.jetbrains.compose.storytale.storiesStorage 5 | import util.invokeMainController 6 | import util.storytaleTest 7 | 8 | class MentionAllStoriesGettersInsideMainFunctionLoweringTest { 9 | @Test 10 | fun `all stories are registered after compilation and main invocation`() { 11 | val result = storytaleTest { 12 | "Group1.story.kt" hasContent """ 13 | package storytale.gallery.demo 14 | import org.jetbrains.compose.storytale.story 15 | 16 | val Story1 by story { } 17 | val Story2 by story { } 18 | """ 19 | "Group2.story.kt" hasContent """ 20 | package storytale.gallery.demo 21 | import org.jetbrains.compose.storytale.story 22 | 23 | val Story3 by story { } 24 | """ 25 | "Main.kt" hasContent """ 26 | package org.jetbrains.compose.storytale.generated 27 | fun MainViewController() { } 28 | """ 29 | } 30 | 31 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 32 | 33 | result.invokeMainController() 34 | 35 | // CRAZY!!! 36 | assertThat(storiesStorage).hasSize(3) 37 | assertThat(storiesStorage.map { it.name }) 38 | .containsExactlyInAnyOrder("Story1", "Story2", "Story3") 39 | assertThat(storiesStorage.map { it.group }.toSet()) 40 | .containsExactlyInAnyOrder("Group1", "Group2") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/composeResources/drawable/wrench.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /gallery-demo/src/wasmJsMain/kotlin/storytale/gallery/demo/Main.kt: -------------------------------------------------------------------------------- 1 | package storytale.gallery.demo 2 | 3 | import androidx.compose.runtime.LaunchedEffect 4 | import androidx.compose.ui.window.ComposeViewport 5 | import androidx.navigation.ExperimentalBrowserHistoryApi 6 | import androidx.navigation.bindToNavigation 7 | import androidx.navigation.compose.rememberNavController 8 | import kotlinx.browser.window 9 | import org.jetbrains.compose.resources.ExperimentalResourceApi 10 | import org.jetbrains.compose.resources.preloadFont 11 | import org.jetbrains.compose.storytale.gallery.material3.StorytaleGalleryApp 12 | import org.jetbrains.compose.storytale.gallery.material3.openFullScreenStory 13 | import org.jetbrains.compose.storytale.gallery.story.code.JetBrainsMonoRegularRes 14 | import org.jetbrains.compose.storytale.generated.MainViewController 15 | 16 | @OptIn(ExperimentalResourceApi::class, ExperimentalBrowserHistoryApi::class) 17 | fun main() { 18 | MainViewController() // Storytale compiler will initialize the stories 19 | 20 | openFullScreenStory = { story, _ -> 21 | window.open(window.location.origin + "/#story/" + story.storyName) 22 | } 23 | 24 | val useEmbedded = window.location.search.contains("embedded=true") 25 | 26 | ComposeViewport(viewportContainerId = "composeApplication") { 27 | val hasResourcePreloadCompleted = preloadFont(JetBrainsMonoRegularRes).value != null 28 | val navHostController = rememberNavController() 29 | 30 | if (hasResourcePreloadCompleted) { 31 | StorytaleGalleryApp(isEmbedded = useEmbedded, navHostController) 32 | 33 | LaunchedEffect(Unit) { 34 | window.bindToNavigation(navHostController) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/NativeSourceGeneratorTask.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.FileSpec 5 | import java.io.File 6 | import org.gradle.api.DefaultTask 7 | import org.gradle.api.tasks.CacheableTask 8 | import org.gradle.api.tasks.Input 9 | import org.gradle.api.tasks.OutputDirectory 10 | import org.gradle.api.tasks.TaskAction 11 | 12 | @CacheableTask 13 | open class NativeSourceGeneratorTask : DefaultTask() { 14 | @Input 15 | lateinit var title: String 16 | 17 | @OutputDirectory 18 | lateinit var outputResourcesDir: File 19 | 20 | @OutputDirectory 21 | lateinit var outputSourcesDir: File 22 | 23 | @TaskAction 24 | fun generate() { 25 | cleanup(outputSourcesDir) 26 | cleanup(outputResourcesDir) 27 | 28 | generateSources() 29 | } 30 | 31 | private fun generateSources() { 32 | val file = FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "Main").apply { 33 | addImport("androidx.compose.ui.window", "ComposeUIViewController") 34 | addImport("org.jetbrains.compose.storytale.gallery", "Gallery") 35 | 36 | val uIViewControllerType = ClassName("platform.UIKit", "UIViewController") 37 | function("MainViewController") { 38 | returns(uIViewControllerType) 39 | addStatement( 40 | """ 41 | |return ComposeUIViewController { 42 | | Gallery() 43 | |} 44 | """.trimMargin(), 45 | ) 46 | } 47 | }.build() 48 | 49 | file.writeTo(outputSourcesDir) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /modules/gradle-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | `java-gradle-plugin` 6 | `maven-publish` 7 | alias(libs.plugins.buildTimeConfig) 8 | } 9 | 10 | gradlePlugin { 11 | plugins { 12 | create("storytale") { 13 | id = "org.jetbrains.compose.storytale" 14 | implementationClass = "org.jetbrains.compose.storytale.plugin.StorytaleGradlePlugin" 15 | } 16 | } 17 | } 18 | 19 | dependencies { 20 | implementation(libs.kotlin.gradle.plugin) 21 | implementation(libs.android.gradle.plugin) 22 | implementation(libs.kotlin.compiler.embeddable) 23 | implementation(libs.compose.gradle.plugin) 24 | implementation(libs.compose.hot.reload.plugin) 25 | implementation(libs.kotlin.poet) 26 | } 27 | 28 | group = "org.jetbrains.compose.storytale" 29 | 30 | val emptyJavadocJar by tasks.registering(Jar::class) { 31 | archiveClassifier.set("javadoc") 32 | } 33 | 34 | publishing { 35 | publications { 36 | create("maven") { 37 | artifactId = "gradle-plugin" 38 | from(components["kotlin"]) 39 | } 40 | withType { 41 | artifact(emptyJavadocJar) 42 | } 43 | } 44 | } 45 | 46 | tasks.withType().configureEach { 47 | friendPaths.setFrom(libraries) 48 | } 49 | 50 | buildTimeConfig { 51 | config { 52 | packageName.set("org.jetbrains.compose.storytale.plugin") 53 | objectName.set("BuildTimeConfig") 54 | destination.set(project.layout.buildDirectory.get().asFile) 55 | 56 | configProperties { 57 | val projectVersion: String by string(version as String) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /modules/compiler-plugin/src/test/kotlin/ReplaceStoryCallWithItsSuccessorWithCodeParameterTest.kt: -------------------------------------------------------------------------------- 1 | import com.tschuchort.compiletesting.KotlinCompilation 2 | import org.assertj.core.api.Assertions.assertThat 3 | import org.jetbrains.compose.storytale.storiesStorage 4 | import org.junit.Test 5 | import util.invokeMainController 6 | import util.storytaleTest 7 | 8 | class ReplaceStoryCallWithItsSuccessorWithCodeParameterTest { 9 | @Test 10 | fun `replace story call extracts code parameter`() { 11 | val result = storytaleTest { 12 | "Group1.story.kt" hasContent """ 13 | package storytale.gallery.demo 14 | import org.jetbrains.compose.storytale.story 15 | import androidx.compose.foundation.text.BasicText 16 | 17 | val Story1 by story { 18 | val text by parameter("Hello World") 19 | BasicText(text) 20 | } 21 | """ 22 | "Main.kt" hasContent """ 23 | package org.jetbrains.compose.storytale.generated 24 | fun MainViewController() { } 25 | """ 26 | } 27 | 28 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 29 | 30 | result.invokeMainController() 31 | 32 | assertThat(storiesStorage).hasSize(1) 33 | with(storiesStorage.first()) { 34 | assertThat(name).isEqualTo("Story1") 35 | assertThat(group).isEqualTo("Group1") 36 | assertThat(code).isEqualTo( 37 | """ 38 | |val text by parameter("Hello World") 39 | |BasicText(text) 40 | """ 41 | .trimMargin() 42 | .trim('\n'), 43 | ) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /modules/compiler-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | `maven-publish` 6 | kotlin("jvm") 7 | alias(libs.plugins.jetbrainsCompose) 8 | alias(libs.plugins.compose.compiler) 9 | } 10 | dependencies { 11 | compileOnly(compose.runtime) 12 | implementation(kotlin("compiler-embeddable")) 13 | testImplementation(compose.foundation) 14 | testImplementation(compose.material3) 15 | testImplementation(compose.runtime) 16 | testImplementation(compose.ui) 17 | testImplementation(kotlin("compose-compiler-plugin-embeddable")) 18 | testImplementation(kotlin("test")) 19 | testImplementation(libs.assertj.core) 20 | testImplementation(libs.junit) 21 | testImplementation(libs.kotlinCompileTesting.core) 22 | testImplementation(project(":modules:runtime-api")) 23 | } 24 | 25 | sourceSets { 26 | val main by getting { 27 | kotlin.srcDirs("src/kotlin") 28 | resources.srcDir("src/resources") 29 | } 30 | val test by getting { 31 | kotlin.srcDirs("src/test/kotlin") 32 | resources.srcDir("src/test/resources") 33 | } 34 | } 35 | 36 | group = "org.jetbrains.compose.storytale" 37 | 38 | val emptyJavadocJar by tasks.registering(Jar::class) { 39 | archiveClassifier.set("javadoc") 40 | } 41 | 42 | publishing { 43 | publications { 44 | create("maven") { 45 | artifactId = "compiler-plugin" 46 | from(components["kotlin"]) 47 | } 48 | withType { 49 | artifact(emptyJavadocJar) 50 | } 51 | } 52 | } 53 | 54 | tasks.withType>().configureEach { 55 | compilerOptions.optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") 56 | } 57 | -------------------------------------------------------------------------------- /examples/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /modules/gallery/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/StorytaleExtension.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import org.gradle.api.Project 4 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 5 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet 6 | 7 | open class StorytaleExtension(internal val project: Project) { 8 | var buildDir: String = "storytale" 9 | 10 | val multiplatformExtension = run { 11 | val multiplatformClass = 12 | tryGetClass( 13 | className = "org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension", 14 | ) 15 | multiplatformClass?.let { project.extensions.findByType(it) } ?: error("UNEXPECTED") 16 | } 17 | 18 | val targets = multiplatformExtension.targets 19 | 20 | val mainStoriesSourceSet by lazy { 21 | multiplatformExtension 22 | .sourceSets 23 | .create("common${StorytaleGradlePlugin.STORYTALE_SOURCESET_SUFFIX}") 24 | .apply { setupCommonStoriesSourceSetDependencies(this) } 25 | } 26 | 27 | internal fun setupCommonStoriesSourceSetDependencies(sourceSet: KotlinSourceSet) { 28 | with(sourceSet) { 29 | dependencies { 30 | implementation("org.jetbrains.compose.storytale:gallery:${StorytaleGradlePlugin.VERSION}") 31 | implementation("org.jetbrains.compose.storytale:runtime-api:${StorytaleGradlePlugin.VERSION}") 32 | } 33 | } 34 | } 35 | 36 | private fun Any.tryGetClass(className: String): Class? { 37 | val classLoader = javaClass.classLoader 38 | return try { 39 | @Suppress("UNCHECKED_CAST") 40 | Class.forName(className, false, classLoader) as Class 41 | } catch (e: ClassNotFoundException) { 42 | null 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/composeResources/drawable/compose-multiplatform.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Application on GitHub Pages 2 | # Don't add something here until the plugin is available on Maven, the CI is quite long :( 3 | on: [workflow_dispatch] 4 | jobs: 5 | # Build job 6 | build: 7 | name: Build Kotlin/Wasm Stories 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Set up JDK 11 | uses: actions/setup-java@v2 12 | with: 13 | distribution: 'adopt' 14 | java-version: '17' 15 | 16 | - name: Checkout code 17 | uses: actions/checkout@v3 18 | 19 | - name: Publish local Storytale plugin 20 | run: ./gradlew publishToMavenLocal 21 | 22 | - name: Run Gradle Tasks 23 | run: | 24 | cd examples 25 | ../gradlew wasmJsBrowserStoriesProductionExecutableDistribution 26 | 27 | - name: Fix permissions 28 | run: | 29 | chmod -v -R +rX "examples/build/dist/wasmJs/StoriesProductionExecutable/" | while read line; do 30 | echo "::warning title=Invalid file permissions automatically fixed::$line" 31 | done 32 | 33 | - name: Upload Pages artifact 34 | uses: actions/upload-pages-artifact@v3 35 | with: 36 | path: examples/build/dist/wasmJs/StoriesProductionExecutable 37 | 38 | deploy: 39 | # Add a dependency to the build job 40 | needs: build 41 | 42 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 43 | permissions: 44 | pages: write # to deploy to Pages 45 | id-token: write # to verify the deployment originates from an appropriate source 46 | 47 | # Deploy to the github-pages environment 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | 52 | # Specify runner + deployment step 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Deploy to GitHub Pages 56 | id: deployment 57 | uses: actions/deploy-pages@v4 58 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/JsSourceGeneratorTask.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import com.squareup.kotlinpoet.FileSpec 4 | import java.io.File 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.tasks.CacheableTask 7 | import org.gradle.api.tasks.Input 8 | import org.gradle.api.tasks.OutputDirectory 9 | import org.gradle.api.tasks.TaskAction 10 | import org.jetbrains.kotlin.incremental.createDirectory 11 | 12 | @CacheableTask 13 | open class JsSourceGeneratorTask : DefaultTask() { 14 | 15 | @Input 16 | lateinit var title: String 17 | 18 | @OutputDirectory 19 | lateinit var outputResourcesDir: File 20 | 21 | @OutputDirectory 22 | lateinit var outputSourcesDir: File 23 | 24 | @TaskAction 25 | fun generate() { 26 | cleanup(outputSourcesDir) 27 | cleanup(outputResourcesDir) 28 | 29 | generateSources() 30 | generateResources() 31 | } 32 | 33 | private fun generateSources() { 34 | val file = FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "Main").apply { 35 | addImport("org.jetbrains.skiko.wasm", "onWasmReady") 36 | addMainFileImports() 37 | addMainViewControllerFun() 38 | function("main") { 39 | addStatement("onWasmReady { MainViewController() }") 40 | } 41 | }.build() 42 | 43 | file.writeTo(outputSourcesDir) 44 | } 45 | 46 | private fun generateResources() { 47 | if (!outputResourcesDir.exists()) { 48 | outputResourcesDir.createDirectory() 49 | } 50 | 51 | val stylesFile = File(outputResourcesDir, "styles.css") 52 | stylesFile.writeText(webStylesCssContent) 53 | 54 | val index = File(outputResourcesDir, "index.html") 55 | index.writeText(webIndexHtmlContent(SCRIPT_FILE_NAME)) 56 | } 57 | 58 | companion object { 59 | internal const val SCRIPT_FILE_NAME = "composeApp.js" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt: -------------------------------------------------------------------------------- 1 | import com.tschuchort.compiletesting.KotlinCompilation 2 | import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.Test 5 | import util.assertableGeneratedKspSources 6 | import util.hasContent 7 | import util.storytaleTest 8 | 9 | class PreviewProcessorAndroidTest { 10 | 11 | @Test 12 | fun `generates story for Androidx Android Preview function`() { 13 | val compilation = storytaleTest { 14 | "AndroidButton.kt" hasContent """ 15 | package storytale.gallery.demo 16 | 17 | @androidx.compose.runtime.Composable 18 | fun AndroidButton() { } 19 | 20 | @androidx.compose.ui.tooling.preview.Preview 21 | @androidx.compose.runtime.Composable 22 | fun PreviewAndroidButton() { 23 | AndroidButton() 24 | } 25 | """ 26 | } 27 | val result = compilation 28 | .compile() 29 | 30 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 31 | 32 | assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1) 33 | 34 | assertThat(result.assertableGeneratedKspSources(compilation)) 35 | .containsExactlyInAnyOrder( 36 | "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """ 37 | |package org.jetbrains.compose.storytale.generated 38 | | 39 | |import org.jetbrains.compose.storytale.Story 40 | |import org.jetbrains.compose.storytale.story 41 | |import storytale.gallery.demo.PreviewAndroidButton 42 | | 43 | |public val AndroidButton: Story by story { 44 | | PreviewAndroidButton() 45 | |} 46 | """.trimMargin(), 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Storytale" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | google { 7 | mavenContent { 8 | includeGroupAndSubgroups("androidx") 9 | includeGroupAndSubgroups("com.android") 10 | includeGroupAndSubgroups("com.google") 11 | } 12 | } 13 | mavenCentral() 14 | gradlePluginPortal() 15 | mavenLocal() 16 | } 17 | } 18 | 19 | dependencyResolutionManagement { 20 | repositories { 21 | google { 22 | mavenContent { 23 | includeGroupAndSubgroups("androidx") 24 | includeGroupAndSubgroups("com.android") 25 | includeGroupAndSubgroups("com.google") 26 | } 27 | } 28 | mavenCentral() 29 | mavenLocal() 30 | exclusiveContent { 31 | forRepository { 32 | maven { 33 | url = uri("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies/") 34 | } 35 | } 36 | filter { 37 | includeGroupByRegex("org\\.jetbrains\\.intellij\\.deps.*") 38 | } 39 | } 40 | exclusiveContent { 41 | forRepository { 42 | maven { 43 | url = uri("https://www.jetbrains.com/intellij-repository/releases/") 44 | } 45 | } 46 | filter { 47 | includeGroup("com.jetbrains.intellij.platform") 48 | } 49 | } 50 | } 51 | } 52 | 53 | plugins { 54 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" 55 | } 56 | 57 | include(":examples") 58 | include(":gallery-demo") 59 | include(":modules:gallery") 60 | includeBuild("modules/gradle-plugin") 61 | include(":modules:compiler-plugin") 62 | include(":modules:dokka-plugin") 63 | include(":modules:runtime-api") 64 | include(":modules:preview-processor") 65 | include(":modules:preview-processor-test") 66 | -------------------------------------------------------------------------------- /gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt: -------------------------------------------------------------------------------- 1 | package storytale.gallery.demo 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.AddCircle 7 | import androidx.compose.material3.ExtendedFloatingActionButton 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.SegmentedButton 11 | import androidx.compose.material3.SegmentedButtonDefaults 12 | import androidx.compose.material3.SingleChoiceSegmentedButtonRow 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.mutableIntStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.unit.dp 19 | import org.jetbrains.compose.storytale.previewParameter 20 | 21 | @org.jetbrains.compose.ui.tooling.preview.Preview 22 | @Composable 23 | @Suppress("ktlint") 24 | private fun PreviewExtendedFAB() { 25 | val bgColor by previewParameter(MaterialTheme.colorScheme.primary) 26 | 27 | ExtendedFloatingActionButton(onClick = {}, containerColor = bgColor) { 28 | Icon(imageVector = Icons.Default.AddCircle, contentDescription = null) 29 | Spacer(Modifier.padding(4.dp)) 30 | Text("Extended") 31 | } 32 | } 33 | 34 | @androidx.compose.desktop.ui.tooling.preview.Preview 35 | @Composable 36 | @Suppress("ktlint") 37 | private fun PreviewSegmentedButton() { 38 | val selectedIndex = remember { mutableIntStateOf(0) } 39 | 40 | SingleChoiceSegmentedButtonRow { 41 | repeat(3) { index -> 42 | SegmentedButton( 43 | selected = index == selectedIndex.value, 44 | onClick = { selectedIndex.value = index }, 45 | shape = SegmentedButtonDefaults.itemShape(index, 3), 46 | ) { 47 | Text("Button $index", modifier = Modifier.padding(4.dp)) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/NativeCopyResourcesTask.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.file.DirectoryProperty 5 | import org.gradle.api.file.FileCollection 6 | import org.gradle.api.provider.Property 7 | import org.gradle.api.tasks.Input 8 | import org.gradle.api.tasks.InputFiles 9 | import org.gradle.api.tasks.OutputDirectory 10 | import org.gradle.api.tasks.PathSensitive 11 | import org.gradle.api.tasks.PathSensitivity 12 | import org.gradle.api.tasks.TaskAction 13 | 14 | internal abstract class NativeCopyResourcesTask : DefaultTask() { 15 | @get:Input 16 | abstract val xcodeTargetPlatform: Property 17 | 18 | @get:Input 19 | abstract val xcodeTargetArchs: Property 20 | 21 | @get:PathSensitive(PathSensitivity.ABSOLUTE) 22 | @get:InputFiles 23 | abstract val resourceFiles: Property 24 | 25 | @get:OutputDirectory 26 | abstract val outputDir: DirectoryProperty 27 | 28 | @TaskAction 29 | fun run() { 30 | val outputDir = outputDir.get().asFile 31 | outputDir.deleteRecursively() 32 | outputDir.mkdirs() 33 | logger.info("Clean ${outputDir.path}") 34 | 35 | resourceFiles.get().forEach { dir -> 36 | if (dir.exists() && dir.isDirectory) { 37 | logger.info("Copy '${dir.path}' to '${outputDir.path}'") 38 | dir.walkTopDown().filter { !it.isDirectory && !it.isHidden }.forEach { file -> 39 | val targetFile = outputDir.resolve(file.relativeTo(dir)) 40 | if (targetFile.exists()) { 41 | logger.info("Skip [already exists] '${file.path}'") 42 | } else { 43 | logger.info(" -> '${file.path}'") 44 | file.copyTo(targetFile) 45 | } 46 | } 47 | } else { 48 | logger.info("File '${dir.path}' is not a dir or doesn't exist") 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/JvmSourceGeneratorTask.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import com.squareup.kotlinpoet.FileSpec 4 | import java.io.File 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.tasks.CacheableTask 7 | import org.gradle.api.tasks.Input 8 | import org.gradle.api.tasks.OutputDirectory 9 | import org.gradle.api.tasks.TaskAction 10 | 11 | @CacheableTask 12 | open class JvmSourceGeneratorTask : DefaultTask() { 13 | @Input 14 | lateinit var title: String 15 | 16 | @OutputDirectory 17 | lateinit var outputResourcesDir: File 18 | 19 | @OutputDirectory 20 | lateinit var outputSourcesDir: File 21 | 22 | @TaskAction 23 | fun generate() { 24 | cleanup(outputSourcesDir) 25 | cleanup(outputResourcesDir) 26 | 27 | generateSources() 28 | } 29 | 30 | private fun generateSources() { 31 | val file = FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "Main").apply { 32 | addImport("androidx.compose.ui.window", "Window") 33 | addImport("androidx.compose.ui.window", "application") 34 | addImport("androidx.compose.ui.unit", "DpSize") 35 | addImport("androidx.compose.ui.unit", "dp") 36 | addImport("androidx.compose.ui.window", "rememberWindowState") 37 | addImport("org.jetbrains.compose.storytale.gallery", "Gallery") 38 | 39 | function("MainViewController") { 40 | addStatement( 41 | """ 42 | |application { 43 | | Window( 44 | | onCloseRequest = ::exitApplication, 45 | | title = "example", 46 | | state = rememberWindowState(size = DpSize(1440.dp, 920.dp)) 47 | | ) { 48 | | Gallery() 49 | | } 50 | |} 51 | """.trimMargin(), 52 | ) 53 | } 54 | 55 | function("main") { 56 | addStatement("MainViewController()") 57 | } 58 | }.build() 59 | 60 | file.writeTo(outputSourcesDir) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/composeResources/drawable/palette.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/ReponsiveNavigationDrawer.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.material3 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.width 5 | import androidx.compose.material3.DismissibleDrawerSheet 6 | import androidx.compose.material3.DismissibleNavigationDrawer 7 | import androidx.compose.material3.DrawerState 8 | import androidx.compose.material3.PermanentDrawerSheet 9 | import androidx.compose.material3.PermanentNavigationDrawer 10 | import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.movableContentOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | import androidx.window.core.layout.WindowWidthSizeClass 17 | 18 | @Composable 19 | fun ResponsiveNavigationDrawer( 20 | drawerState: DrawerState, 21 | drawerContent: @Composable ColumnScope.() -> Unit, 22 | content: @Composable () -> Unit, 23 | ) { 24 | val currentWindowWidthClass = currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass 25 | val isWideWindow = currentWindowWidthClass == WindowWidthSizeClass.EXPANDED 26 | val isSmallWindow = currentWindowWidthClass == WindowWidthSizeClass.COMPACT 27 | 28 | val drawerModifier = Modifier.width(280.dp) 29 | if (!isWideWindow) { 30 | DismissibleNavigationDrawer( 31 | drawerContent = remember { 32 | movableContentOf { 33 | DismissibleDrawerSheet( 34 | drawerState = drawerState, 35 | content = drawerContent, 36 | modifier = drawerModifier, 37 | ) 38 | } 39 | }, 40 | content = content, 41 | drawerState = drawerState, 42 | gesturesEnabled = isSmallWindow, 43 | ) 44 | } else { 45 | PermanentNavigationDrawer( 46 | drawerContent = { 47 | PermanentDrawerSheet(content = drawerContent, modifier = drawerModifier) 48 | }, 49 | content = content, 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /modules/dokka-plugin/src/main/kotlin/org/jetbrains/dokka/storytale/StorytalePlugin.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package org.jetbrains.dokka.storytale 6 | 7 | import org.jetbrains.dokka.CoreExtensions 8 | import org.jetbrains.dokka.DokkaConfiguration 9 | import org.jetbrains.dokka.base.DokkaBase 10 | import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider 11 | import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder 12 | import org.jetbrains.dokka.model.doc.CustomTagWrapper 13 | import org.jetbrains.dokka.model.doc.Text 14 | import org.jetbrains.dokka.plugability.DokkaPlugin 15 | import org.jetbrains.dokka.plugability.DokkaPluginApiPreview 16 | import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement 17 | 18 | public class StorytalePlugin : DokkaPlugin() { 19 | private val dokkaBase by lazy { plugin() } 20 | 21 | public val storytaleRenderer by extending { 22 | CoreExtensions.renderer providing { StoryHtmlRenderer(it) } override dokkaBase.htmlRenderer 23 | } 24 | 25 | public val storyTagContentProvider by extending { 26 | plugin().customTagContentProvider with StoryTagContentProvider order { 27 | before(plugin().sinceKotlinTagContentProvider) 28 | } 29 | } 30 | 31 | @OptIn(DokkaPluginApiPreview::class) 32 | override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement 33 | } 34 | 35 | private const val STORY_TAG = "story" 36 | 37 | private object StoryTagContentProvider : CustomTagContentProvider { 38 | override fun isApplicable(customTag: CustomTagWrapper): Boolean = customTag.name == STORY_TAG 39 | 40 | override fun DocumentableContentBuilder.contentForDescription( 41 | sourceSet: DokkaConfiguration.DokkaSourceSet, 42 | customTag: CustomTagWrapper, 43 | ) { 44 | val url = customTag.extractUrl() ?: return 45 | 46 | codeBlock(language = STORY_IFRAME_CODE) { text(url) } 47 | } 48 | 49 | private fun CustomTagWrapper.extractUrl(): String? = (((root.children.firstOrNull() as? org.jetbrains.dokka.model.doc.P)?.children as? ArrayList)?.singleOrNull() as? Text)?.body 50 | } 51 | -------------------------------------------------------------------------------- /modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryParameterDelegate.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import kotlin.reflect.KClass 6 | import kotlin.reflect.KProperty 7 | 8 | class StoryParameterDelegate( 9 | private val story: Story, 10 | private val type: KClass<*>, 11 | private val defaultValue: T, 12 | private val label: String? = null, 13 | ) { 14 | @Suppress("UNCHECKED_CAST") 15 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T = story.nameToParameterMapping.getValue(property.name).state.value as T 16 | 17 | @Suppress("UNCHECKED_CAST") 18 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 19 | (story.nameToParameterMapping.getValue(property.name).state as MutableState).value = value 20 | } 21 | 22 | operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) = also { 23 | story.nameToParameterMapping.getOrPut(property.name) { 24 | StoryParameter(property.name, type, values = null, label, mutableStateOf(defaultValue)) 25 | } 26 | } 27 | } 28 | 29 | class StoryListParameterDelegate( 30 | private val story: Story, 31 | private val type: KClass<*>, 32 | private val list: List, 33 | private val defaultValueIndex: Int, 34 | private val label: String? = null, 35 | ) { 36 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T = list[story.nameToParameterMapping.getValue(property.name).state.value as Int] 37 | 38 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 39 | val index = list.indexOf(value) 40 | check(index != -1) { 41 | "Cannot find element with value: $value in ${list.joinToString()}." 42 | } 43 | @Suppress("UNCHECKED_CAST") 44 | (story.nameToParameterMapping.getValue(property.name).state as MutableState).value = 45 | index 46 | } 47 | 48 | operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) = also { 49 | story.nameToParameterMapping.getOrPut(property.name) { 50 | require(list.isNotEmpty()) { "List cannot be empty" } 51 | StoryParameter(property.name, type, list, label, mutableStateOf(defaultValueIndex)) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.descriptors.Visibilities 2 | import org.jetbrains.kotlin.fir.FirSession 3 | import org.jetbrains.kotlin.fir.declarations.FirDeclaration 4 | import org.jetbrains.kotlin.fir.declarations.FirDeclarationStatus 5 | import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction 6 | import org.jetbrains.kotlin.fir.declarations.hasAnnotation 7 | import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar 8 | import org.jetbrains.kotlin.fir.extensions.FirStatusTransformerExtension 9 | import org.jetbrains.kotlin.fir.extensions.transform 10 | import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol 11 | import org.jetbrains.kotlin.name.ClassId 12 | import org.jetbrains.kotlin.name.FqName 13 | 14 | class MakePreviewPublicFirExtensionRegistrar : FirExtensionRegistrar() { 15 | companion object { 16 | private val PREVIEW_ANNOTATION_FQ_NAME = FqName("org.jetbrains.compose.ui.tooling.preview.Preview") 17 | private val ANDROIDX_PREVIEW_ANNOTATION_FQ_NAME = FqName("androidx.compose.ui.tooling.preview.Preview") 18 | private val DESKTOP_PREVIEW_ANNOTATION_FQ_NAME = FqName("androidx.compose.desktop.ui.tooling.preview.Preview") 19 | 20 | private val PREVIEW_CLASS_IDS = setOf( 21 | ClassId.topLevel(PREVIEW_ANNOTATION_FQ_NAME), 22 | ClassId.topLevel(ANDROIDX_PREVIEW_ANNOTATION_FQ_NAME), 23 | ClassId.topLevel(DESKTOP_PREVIEW_ANNOTATION_FQ_NAME), 24 | ) 25 | } 26 | 27 | override fun ExtensionRegistrarContext.configurePlugin() { 28 | +FirStatusTransformerExtension.Factory { session -> Extension(session) } 29 | } 30 | 31 | class Extension(session: FirSession) : FirStatusTransformerExtension(session) { 32 | override fun needTransformStatus(declaration: FirDeclaration): Boolean { 33 | if (declaration !is FirSimpleFunction) return false 34 | return PREVIEW_CLASS_IDS.any { declaration.hasAnnotation(it, session) } 35 | } 36 | 37 | override fun transformStatus( 38 | status: FirDeclarationStatus, 39 | function: FirSimpleFunction, 40 | containingClass: FirClassLikeSymbol<*>?, 41 | isLocal: Boolean, 42 | ): FirDeclarationStatus { 43 | return status.transform(visibility = Visibilities.Public) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /modules/compiler-plugin/src/test/kotlin/util/StorytaleTest.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar 4 | import com.tschuchort.compiletesting.JvmCompilationResult 5 | import com.tschuchort.compiletesting.KotlinCompilation 6 | import com.tschuchort.compiletesting.SourceFile 7 | import kotlin.contracts.ExperimentalContracts 8 | import kotlin.contracts.InvocationKind 9 | import kotlin.contracts.contract 10 | import org.assertj.core.api.Assertions 11 | import org.intellij.lang.annotations.Language 12 | import org.jetbrains.compose.plugin.storytale.compiler.StorytaleComponentRegistrar 13 | import org.jetbrains.compose.storytale.storiesStorage 14 | 15 | @OptIn(ExperimentalContracts::class) 16 | fun storytaleTest( 17 | compilationBuilder: KotlinCompilation.() -> Unit = {}, 18 | testSourceBuilder: StorytaleTestSourceScope.() -> Unit, 19 | ): JvmCompilationResult { 20 | contract { 21 | callsInPlace(compilationBuilder, InvocationKind.EXACTLY_ONCE) 22 | callsInPlace(testSourceBuilder, InvocationKind.EXACTLY_ONCE) 23 | } 24 | storiesStorage.clear() 25 | 26 | val testSourceScope = object : StorytaleTestSourceScope { 27 | val sources = mutableListOf() 28 | override fun String.hasContent(content: String) { 29 | sources.add(SourceFile.Companion.kotlin(this, content)) 30 | } 31 | } 32 | 33 | val compilationSetup = KotlinCompilation().apply { 34 | compilerPluginRegistrars = 35 | listOf(StorytaleComponentRegistrar(), ComposePluginRegistrar()) 36 | inheritClassPath = true 37 | messageOutputStream = System.out // see diagnostics in real time 38 | jvmTarget = "17" 39 | verbose = false 40 | } 41 | 42 | compilationSetup.compilationBuilder() 43 | testSourceScope.testSourceBuilder() 44 | 45 | compilationSetup.sources = testSourceScope.sources 46 | 47 | return compilationSetup.compile() 48 | } 49 | 50 | interface StorytaleTestSourceScope { 51 | infix fun String.hasContent(@Language("kotlin") content: String) 52 | } 53 | 54 | fun JvmCompilationResult.invokeMainController() { 55 | val clazz = classLoader.loadClass("org.jetbrains.compose.storytale.generated.MainKt") 56 | 57 | Assertions.assertThat(clazz).isNotNull 58 | Assertions.assertThat(clazz).hasDeclaredMethods("MainViewController") 59 | 60 | clazz.getMethod("MainViewController").invoke(null) 61 | } 62 | -------------------------------------------------------------------------------- /modules/preview-processor-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.compileOnly 2 | import org.gradle.kotlin.dsl.kotlin 3 | import org.gradle.kotlin.dsl.project 4 | import org.gradle.kotlin.dsl.withType 5 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask 6 | 7 | plugins { 8 | alias(libs.plugins.kotlinMultiplatform) 9 | alias(libs.plugins.androidLibrary) 10 | alias(libs.plugins.jetbrainsCompose) 11 | alias(libs.plugins.compose.compiler) 12 | } 13 | 14 | kotlin { 15 | jvm() 16 | androidTarget() 17 | 18 | sourceSets { 19 | val commonMain by getting { 20 | dependencies { 21 | compileOnly(compose.runtime) 22 | } 23 | } 24 | val jvmMain by getting { 25 | dependencies { 26 | implementation(compose.components.uiToolingPreview) 27 | implementation(project(":modules:preview-processor")) 28 | implementation(kotlin("test")) 29 | 30 | implementation(compose.runtime) 31 | implementation(kotlin("compiler-embeddable")) 32 | implementation(kotlin("compose-compiler-plugin-embeddable")) 33 | implementation(kotlin("test")) 34 | implementation(libs.assertj.core) 35 | implementation(libs.junit) 36 | implementation(libs.kotlinCompileTesting.ksp) 37 | implementation(project(":modules:runtime-api")) 38 | } 39 | } 40 | val androidUnitTest by getting { 41 | dependsOn(jvmMain) 42 | 43 | dependencies { 44 | implementation("androidx.compose.ui:ui-tooling-preview-android:1.7.0") 45 | } 46 | } 47 | val jvmTest by getting { 48 | dependencies { 49 | implementation("androidx.compose.ui:ui-tooling-preview-desktop:1.7.0") 50 | } 51 | } 52 | } 53 | } 54 | 55 | android { 56 | compileSdk = 35 57 | namespace = "org.jetbrains.compose.storytale.preview.processor.test" 58 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 59 | defaultConfig { 60 | minSdk = 24 61 | } 62 | compileOptions { 63 | sourceCompatibility = JavaVersion.VERSION_17 64 | targetCompatibility = JavaVersion.VERSION_17 65 | } 66 | } 67 | 68 | tasks.withType>().configureEach { 69 | compilerOptions.optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") 70 | } 71 | -------------------------------------------------------------------------------- /modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import PreviewComponentRegistrar 4 | import PreviewProcessor 5 | import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar 6 | import com.tschuchort.compiletesting.JvmCompilationResult 7 | import com.tschuchort.compiletesting.KotlinCompilation 8 | import com.tschuchort.compiletesting.SourceFile 9 | import com.tschuchort.compiletesting.kspSourcesDir 10 | import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor 11 | import com.tschuchort.compiletesting.symbolProcessorProviders 12 | import com.tschuchort.compiletesting.useKsp2 13 | import kotlin.contracts.ExperimentalContracts 14 | import kotlin.contracts.InvocationKind 15 | import kotlin.contracts.contract 16 | import org.intellij.lang.annotations.Language 17 | import org.jetbrains.kotlin.utils.fileUtils.descendantRelativeTo 18 | 19 | @OptIn(ExperimentalContracts::class) 20 | fun storytaleTest( 21 | compilationBuilder: KotlinCompilation.() -> Unit = {}, 22 | testSourceBuilder: StorytaleTestSourceScope.() -> Unit, 23 | ): KotlinCompilation { 24 | contract { 25 | callsInPlace(compilationBuilder, InvocationKind.EXACTLY_ONCE) 26 | } 27 | 28 | val testSourceScope = object : StorytaleTestSourceScope { 29 | val sources = mutableListOf() 30 | override fun String.hasContent(content: String) { 31 | sources.add(SourceFile.Companion.kotlin(this, content)) 32 | } 33 | } 34 | 35 | return KotlinCompilation().apply { 36 | compilerPluginRegistrars = listOf( 37 | ComposePluginRegistrar(), 38 | PreviewComponentRegistrar(), 39 | ) 40 | useKsp2() 41 | symbolProcessorProviders.add(PreviewProcessor.Provider()) 42 | 43 | // magic 44 | inheritClassPath = true 45 | messageOutputStream = System.out // see diagnostics in real time 46 | jvmTarget = "17" 47 | verbose = false 48 | 49 | compilationBuilder() 50 | testSourceScope.testSourceBuilder() 51 | sources = testSourceScope.sources 52 | } 53 | } 54 | 55 | interface StorytaleTestSourceScope { 56 | infix fun String.hasContent(@Language("kotlin") content: String) 57 | } 58 | 59 | fun JvmCompilationResult.assertableGeneratedKspSources( 60 | compilation: KotlinCompilation, 61 | ): List { 62 | return sourcesGeneratedBySymbolProcessor.toList().map { 63 | it.descendantRelativeTo(compilation.kspSourcesDir).path hasContent 64 | org.assertj.core.util.Files.contentOf(it, Charsets.UTF_8).trim() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StorytaleGalleryApp.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.material3 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.lightColorScheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.mutableStateSetOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.platform.LocalDensity 14 | import androidx.compose.ui.unit.Density 15 | import androidx.navigation.NavHostController 16 | import androidx.navigation.compose.rememberNavController 17 | import org.jetbrains.compose.storytale.gallery.ui.theme.LocalCustomDensity 18 | 19 | @Composable 20 | fun StorytaleGalleryApp( 21 | isEmbedded: Boolean = false, 22 | navHostController: NavHostController = rememberNavController(), 23 | ) { 24 | val isSystemDarkTheme = isSystemInDarkTheme() 25 | 26 | val appState = remember { 27 | StorytaleGalleryAppState(isSystemDarkTheme) 28 | } 29 | 30 | CompositionLocalProvider( 31 | LocalCustomDensity provides Density(LocalDensity.current.density * 0.8f), 32 | ) { 33 | MaterialTheme(colorScheme = if (appState.isDarkTheme()) darkThemeColors else lightThemeColors) { 34 | if (isEmbedded) { 35 | EmbeddedStoryView(appState, navHostController) 36 | } else { 37 | FullStorytaleGallery(appState, navHostController) 38 | } 39 | } 40 | } 41 | 42 | LaunchedEffect(isSystemDarkTheme) { 43 | appState.switchTheme(isSystemDarkTheme) 44 | } 45 | } 46 | 47 | internal val darkThemeColors = darkColorScheme() 48 | internal val lightThemeColors = lightColorScheme() 49 | 50 | @Composable 51 | internal fun isDarkMaterialTheme(): Boolean { 52 | return MaterialTheme.colorScheme == darkThemeColors 53 | } 54 | 55 | class StorytaleGalleryAppState( 56 | initialIsDarkTheme: Boolean, 57 | ) { 58 | private val localIsDarkTheme = mutableStateOf(false) 59 | 60 | init { 61 | localIsDarkTheme.value = initialIsDarkTheme 62 | } 63 | 64 | val expandedGroups = mutableStateSetOf() 65 | 66 | fun switchTheme(dark: Boolean) { 67 | localIsDarkTheme.value = dark 68 | } 69 | 70 | fun isDarkTheme(): Boolean = localIsDarkTheme.value 71 | } 72 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/ui/component/Gap.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.ui.component 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.Dp 12 | import androidx.compose.ui.unit.isSpecified 13 | 14 | /** 15 | * A spacer that adds a vertical gap between elements in a column layout. 16 | * 17 | * @param height The height of the gap. 18 | * @param modifier The optional modifier to be applied to the spacer. 19 | */ 20 | @Composable 21 | fun ColumnScope.Gap(height: Dp, modifier: Modifier = Modifier) = Spacer(modifier = modifier.height(height)) 22 | 23 | /** 24 | * A spacer that adds a horizontal gap between elements in a row layout. 25 | * 26 | * @param width The width of the gap. 27 | * @param modifier The optional modifier to be applied to the spacer. 28 | */ 29 | @Composable 30 | fun RowScope.Gap(width: Dp, modifier: Modifier = Modifier) = Spacer(modifier = modifier.width(width)) 31 | 32 | /** 33 | * A spacer that fills the available space in a column layout. 34 | * 35 | * @param modifier The optional modifier to be applied to the spacer. 36 | */ 37 | @Composable 38 | fun ColumnScope.FillGap(modifier: Modifier = Modifier) = Spacer(modifier = modifier.weight(1f)) 39 | 40 | /** 41 | * A spacer that fills the available space in a row layout. 42 | * 43 | * @param modifier The optional modifier to be applied to the spacer. 44 | */ 45 | @Composable 46 | fun RowScope.FillGap(modifier: Modifier = Modifier) = Spacer(modifier = modifier.weight(1f)) 47 | 48 | /** 49 | * A spacer that adds a square gap of the specified size. 50 | * 51 | * @param size The size of the gap. 52 | * @param modifier The optional modifier to be applied to the spacer. 53 | */ 54 | @Composable 55 | fun Gap(size: Dp, modifier: Modifier = Modifier) = Spacer(modifier = modifier.size(size)) 56 | 57 | /** 58 | * A spacer that adds a gap with the specified width and height. 59 | * 60 | * @param modifier The optional modifier to be applied to the spacer. 61 | * @param width The width of the gap. If not specified, the width will be unspecified. 62 | * @param height The height of the gap. If not specified, the height will be unspecified. 63 | */ 64 | @Composable 65 | fun Gap( 66 | modifier: Modifier = Modifier, 67 | width: Dp = Dp.Unspecified, 68 | height: Dp = Dp.Unspecified, 69 | ) = Spacer( 70 | modifier = modifier 71 | .run { if (width.isSpecified) width(width) else this } 72 | .run { if (height.isSpecified) height(height) else this }, 73 | ) 74 | -------------------------------------------------------------------------------- /modules/compiler-plugin/src/test/kotlin/StorytaleComponentRegistrarLearningTest.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar 2 | import com.tschuchort.compiletesting.KotlinCompilation 3 | import com.tschuchort.compiletesting.SourceFile 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.jetbrains.compose.plugin.storytale.compiler.StorytaleComponentRegistrar 6 | import org.jetbrains.compose.storytale.storiesStorage 7 | import org.junit.Test 8 | 9 | /** 10 | * This test is a learning test for new comers. Please don't over engineer it 11 | */ 12 | class StorytaleComponentRegistrarLearningTest { 13 | 14 | @Test 15 | fun `all stories are registered after compilation and main invocation`() { 16 | // reset to initialize state 17 | storiesStorage.clear() 18 | 19 | val group1Kt = SourceFile.kotlin( 20 | "Group1.story.kt", 21 | """ 22 | package storytale.gallery.demo 23 | import org.jetbrains.compose.storytale.story 24 | 25 | val Story1 by story { } 26 | val Story2 by story { } 27 | """, 28 | ) 29 | val group2Kt = SourceFile.kotlin( 30 | "Group2.story.kt", 31 | """ 32 | package storytale.gallery.demo 33 | import org.jetbrains.compose.storytale.story 34 | 35 | val Story3 by story { } 36 | """, 37 | ) 38 | val mainKt = SourceFile.kotlin( 39 | "Main.kt", 40 | """ 41 | package org.jetbrains.compose.storytale.generated 42 | fun MainViewController() { } 43 | """, 44 | ) 45 | 46 | val result = KotlinCompilation().apply { 47 | sources = listOf(group1Kt, group2Kt, mainKt) 48 | 49 | compilerPluginRegistrars = 50 | listOf(StorytaleComponentRegistrar(), ComposePluginRegistrar()) 51 | // magic 52 | inheritClassPath = true 53 | messageOutputStream = System.out // see diagnostics in real time 54 | jvmTarget = "17" 55 | verbose = false 56 | } 57 | .compile() 58 | 59 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 60 | 61 | val clazz = result.classLoader.loadClass("org.jetbrains.compose.storytale.generated.MainKt") 62 | 63 | assertThat(clazz).isNotNull 64 | assertThat(clazz).hasDeclaredMethods("MainViewController") 65 | 66 | clazz.getMethod("MainViewController").invoke(null) 67 | 68 | // CRAZY!!! 69 | assertThat(storiesStorage).hasSize(3) 70 | assertThat(storiesStorage.map { it.name }) 71 | .containsExactlyInAnyOrder("Story1", "Story2", "Story3") 72 | assertThat(storiesStorage.map { it.group }.toSet()) 73 | .containsExactlyInAnyOrder("Group1", "Group2") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /modules/dokka-plugin/src/test/kotlin/org/jetbrains/dokka/storytale/StorytalePluginTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.dokka.storytale 2 | 3 | import kotlin.test.assertEquals 4 | import kotlin.test.assertTrue 5 | import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest 6 | import org.jsoup.Jsoup 7 | import org.junit.jupiter.api.Test 8 | import utils.TestOutputWriterPlugin 9 | 10 | class StorytalePluginTest : BaseAbstractTest() { 11 | private val configuration = dokkaConfiguration { 12 | sourceSets { 13 | sourceSet { 14 | sourceRoots = listOf("src/main/kotlin") 15 | } 16 | } 17 | } 18 | 19 | @Test 20 | fun `test iframe generation with story tag`() { 21 | val writerPlugin = TestOutputWriterPlugin() 22 | val source = """ 23 | |/src/main/kotlin/test/Test.kt 24 | |package example 25 | | 26 | |/** 27 | | * Function with story tag 28 | | * @story https://storytale.io/stories/12345 29 | | */ 30 | |fun testWithStory(): String = "Has story" 31 | """.trimIndent() 32 | 33 | testInline( 34 | source, 35 | configuration, 36 | pluginOverrides = listOf(writerPlugin, StorytalePlugin()), 37 | ) { 38 | renderingStage = { _, _ -> 39 | val html = writerPlugin.writer.contents.getValue("root/example/test-with-story.html") 40 | val doc = Jsoup.parse(html) 41 | val iframes = doc.select("iframe") 42 | 43 | assertTrue(iframes.isNotEmpty(), "Should have an iframe") 44 | assertEquals( 45 | "https://storytale.io/stories/12345", 46 | iframes.first()?.attr("src"), 47 | "Iframe should have correct URL", 48 | ) 49 | } 50 | } 51 | } 52 | 53 | @Test 54 | fun `test no iframe without story tag`() { 55 | val writerPlugin = TestOutputWriterPlugin() 56 | val source = """ 57 | |/src/main/kotlin/test/Test.kt 58 | |package example 59 | | 60 | |/** 61 | | * Regular function without story tag 62 | | */ 63 | |fun testWithoutStory(): String = "No story" 64 | """.trimIndent() 65 | 66 | testInline( 67 | source, 68 | configuration, 69 | pluginOverrides = listOf(writerPlugin, StorytalePlugin()), 70 | ) { 71 | renderingStage = { _, _ -> 72 | val html = writerPlugin.writer.contents.getValue("root/example/test-without-story.html") 73 | val doc = Jsoup.parse(html) 74 | val iframes = doc.select("iframe") 75 | 76 | assertTrue(iframes.isEmpty(), "Should not have any iframes") 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /modules/dokka-plugin/src/main/kotlin/org/jetbrains/dokka/storytale/StoryHtmlRenderer.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.dokka.storytale 2 | 3 | import java.util.Locale 4 | import kotlinx.html.FlowContent 5 | import kotlinx.html.code 6 | import kotlinx.html.div 7 | import kotlinx.html.iframe 8 | import kotlinx.html.pre 9 | import kotlinx.html.span 10 | import kotlinx.html.style 11 | import org.jetbrains.dokka.base.renderers.html.HtmlRenderer 12 | import org.jetbrains.dokka.pages.ContentCodeBlock 13 | import org.jetbrains.dokka.pages.ContentPage 14 | import org.jetbrains.dokka.pages.ContentStyle 15 | import org.jetbrains.dokka.pages.ContentText 16 | import org.jetbrains.dokka.pages.TextStyle 17 | import org.jetbrains.dokka.plugability.DokkaContext 18 | 19 | const val STORY_IFRAME_CODE = "@story/iframe" 20 | 21 | open class StoryHtmlRenderer(context: DokkaContext) : HtmlRenderer(context) { 22 | override fun FlowContent.buildCodeBlock(code: ContentCodeBlock, pageContext: ContentPage) { 23 | if (code.language == STORY_IFRAME_CODE) { 24 | div(classes = "storytale-embedded-container") { 25 | iframe(classes = "storytale-embedded") { 26 | style = "display: flex; margin: 15px auto;" 27 | src = (code.children.single() as ContentText).text 28 | attributes["allow"] = "clipboard-read; clipboard-write" 29 | } 30 | } 31 | } else { 32 | div("sample-container") { 33 | val codeLang = "lang-" + code.language.ifEmpty { "kotlin" } 34 | val stylesWithBlock = code.style + TextStyle.Block + codeLang 35 | pre { 36 | code(stylesWithBlock.joinToString(" ") { it.toString().lowercase(Locale.getDefault()) }) { 37 | attributes["theme"] = "idea" 38 | code.children.forEach { buildContentNode(it, pageContext) } 39 | } 40 | } 41 | fun FlowContent.copiedPopup( 42 | notificationContent: String, 43 | additionalClasses: String = "", 44 | ) = div("copy-popup-wrapper $additionalClasses") { 45 | span("copy-popup-icon") 46 | span { 47 | text(notificationContent) 48 | } 49 | } 50 | 51 | fun FlowContent.copyButton() = span(classes = "top-right-position") { 52 | span("copy-icon") 53 | copiedPopup("Content copied to clipboard", "popup-to-left") 54 | } 55 | /* 56 | Disable copy button on samples as: 57 | - it is useless 58 | - it overflows with playground's run button 59 | */ 60 | if (!code.style.contains(ContentStyle.RunnableSample)) copyButton() 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/StorytaleGradlePlugin.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import org.gradle.api.Project 4 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation 5 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin 6 | import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact 7 | import org.jetbrains.kotlin.gradle.plugin.SubpluginOption 8 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget 9 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 10 | import org.jetbrains.kotlin.gradle.targets.js.KotlinWasmTargetType 11 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget 12 | import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget 13 | 14 | class StorytaleGradlePlugin : KotlinCompilerPluginSupportPlugin { 15 | 16 | override fun apply(project: Project) { 17 | project.plugins.withId("org.jetbrains.kotlin.multiplatform") { 18 | project.plugins.withId("org.jetbrains.compose") { 19 | val extension = 20 | project.extensions.create(STORYTALE_EXTENSION_NAME, StorytaleExtension::class.java, project) 21 | project.processConfigurations(extension) 22 | } 23 | } 24 | } 25 | 26 | override fun getCompilerPluginId() = COMPILER_PLUGIN_ID 27 | 28 | override fun isApplicable(kotlinCompilation: KotlinCompilation<*>) = true 29 | 30 | override fun getPluginArtifact() = SubpluginArtifact("org.jetbrains.compose.storytale", "compiler-plugin", VERSION) 31 | 32 | override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>) = kotlinCompilation.target.project.provider { emptyList() } 33 | 34 | private fun Project.processConfigurations(extension: StorytaleExtension) { 35 | extension.targets.all { 36 | when (this) { 37 | is KotlinJsIrTarget -> 38 | when (wasmTargetType) { 39 | KotlinWasmTargetType.JS -> processWasmCompilation(extension, this) 40 | null -> processJsCompilation(extension, this) 41 | else -> {} 42 | } 43 | is KotlinAndroidTarget -> processAndroidCompilation(extension, this) 44 | is KotlinJvmTarget -> processJvmCompilation(extension, this) 45 | is KotlinNativeTarget -> processNativeCompilation(extension, this) 46 | } 47 | } 48 | } 49 | 50 | companion object { 51 | const val COMPILER_PLUGIN_ID = "org.jetbrains.compose.storytale.compiler-plugin" 52 | const val STORYTALE_TASK_GROUP = "storytale" 53 | const val STORYTALE_EXTENSION_NAME = "storytale" 54 | const val STORYTALE_PACKAGE = "org.jetbrains.compose.storytale.generated" 55 | const val STORYTALE_GENERATE_SUFFIX = "StorytaleGenerate" 56 | const val STORYTALE_SOURCESET_SUFFIX = "Stories" 57 | const val STORYTALE_EXEC_SUFFIX = "Stories" 58 | const val STORYTALE_EXEC_PREFIX = "stories" 59 | const val STORYTALE_NATIVE_APP_NAME = "StorytaleGalleryApp" 60 | const val STORYTALE_NATIVE_PROJECT_NAME = "StorytaleXCode" 61 | const val STORYTALE_NATIVE_PROJECT_PATH = "Compose.StorytaleXCode" 62 | const val STORYTALE_DEVICE_NAME = "Storytale iOS Device" 63 | const val DERIVED_DATA_DIRECTORY_NAME = "dd" 64 | const val LINK_BUILD_VERSION = "Debug" 65 | 66 | val VERSION = BuildTimeConfig.projectVersion 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/story/code/CodeBlock.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.story.code 2 | 3 | import androidx.compose.foundation.horizontalScroll 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.text.selection.SelectionContainer 9 | import androidx.compose.foundation.verticalScroll 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.derivedStateOf 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.text.SpanStyle 19 | import androidx.compose.ui.text.buildAnnotatedString 20 | import androidx.compose.ui.text.font.FontFamily 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import dev.snipme.highlights.Highlights 25 | import dev.snipme.highlights.model.BoldHighlight 26 | import dev.snipme.highlights.model.ColorHighlight 27 | import dev.snipme.highlights.model.SyntaxLanguage 28 | import dev.snipme.highlights.model.SyntaxTheme 29 | import dev.snipme.highlights.model.SyntaxThemes 30 | import org.jetbrains.compose.resources.Font 31 | import org.jetbrains.compose.storytale.gallery.compose.text 32 | import org.jetbrains.compose.storytale.gallery.generated.resources.JetBrainsMono_Regular 33 | import org.jetbrains.compose.storytale.gallery.generated.resources.Res 34 | 35 | @Composable 36 | fun CodeBlock( 37 | code: String, 38 | modifier: Modifier = Modifier, 39 | theme: SyntaxTheme = SyntaxThemes.pastel(), 40 | ) = Row(modifier = modifier) { 41 | val codeVerticalScrollState = rememberScrollState() 42 | val codeHighlights by remember(theme, code) { 43 | derivedStateOf { 44 | Highlights.Builder() 45 | .code(code) 46 | .theme(theme) 47 | .language(SyntaxLanguage.KOTLIN) 48 | .build() 49 | } 50 | } 51 | SelectionContainer(Modifier.fillMaxSize()) { 52 | Text( 53 | text = buildAnnotatedString { 54 | text(codeHighlights.getCode()) 55 | codeHighlights.getHighlights() 56 | .filterIsInstance() 57 | .forEach { 58 | addStyle( 59 | SpanStyle(color = Color(it.rgb).copy(alpha = 1f)), 60 | start = it.location.start, 61 | end = it.location.end, 62 | ) 63 | } 64 | codeHighlights.getHighlights() 65 | .filterIsInstance() 66 | .forEach { 67 | addStyle( 68 | SpanStyle(fontWeight = FontWeight.Bold), 69 | start = it.location.start, 70 | end = it.location.end, 71 | ) 72 | } 73 | }, 74 | color = MaterialTheme.colorScheme.onSurface, 75 | fontSize = 14.sp, 76 | lineHeight = 18.sp, 77 | fontFamily = FontFamily(Font(Res.font.JetBrainsMono_Regular)), 78 | modifier = Modifier 79 | .fillMaxSize() 80 | .horizontalScroll(rememberScrollState()) 81 | .verticalScroll(codeVerticalScrollState) 82 | .padding(12.dp), 83 | ) 84 | } 85 | } 86 | 87 | val JetBrainsMonoRegularRes = Res.font.JetBrainsMono_Regular 88 | -------------------------------------------------------------------------------- /modules/gallery/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | 4 | plugins { 5 | alias(libs.plugins.androidLibrary) 6 | alias(libs.plugins.kotlinMultiplatform) 7 | alias(libs.plugins.jetbrainsCompose) 8 | alias(libs.plugins.compose.compiler) 9 | alias(libs.plugins.serialization) 10 | `maven-publish` 11 | } 12 | 13 | kotlin { 14 | js { 15 | browser() 16 | } 17 | wasmJs { 18 | browser() 19 | } 20 | iosX64() 21 | iosArm64() 22 | iosSimulatorArm64() 23 | jvm { 24 | compilerOptions { 25 | jvmTarget.set(JvmTarget.JVM_11) 26 | } 27 | } 28 | androidTarget { 29 | publishLibraryVariants("release") 30 | 31 | compilerOptions { 32 | jvmTarget.set(JvmTarget.JVM_11) 33 | } 34 | } 35 | 36 | applyDefaultHierarchyTemplate() 37 | 38 | sourceSets { 39 | val commonMain by getting { 40 | dependencies { 41 | implementation(compose.runtime) 42 | implementation(compose.foundation) 43 | implementation(compose.material3) 44 | implementation(libs.material3.adaptive) 45 | implementation(libs.material3.icons.core) 46 | implementation(compose.ui) 47 | implementation(compose.components.resources) 48 | implementation(compose.components.uiToolingPreview) 49 | implementation(libs.navigation.compose) 50 | implementation(libs.compose.highlights) 51 | implementation(libs.kotlinx.serialization.json) 52 | implementation(projects.modules.runtimeApi) 53 | implementation(libs.navigation.compose) 54 | } 55 | } 56 | 57 | val mobileMain by creating { 58 | dependsOn(commonMain) 59 | } 60 | 61 | val androidMain by getting { 62 | dependsOn(mobileMain) 63 | } 64 | 65 | val iosMain by getting { 66 | dependsOn(mobileMain) 67 | } 68 | 69 | val desktopMain by creating { 70 | dependsOn(commonMain) 71 | } 72 | 73 | val jsMain by getting { 74 | dependsOn(desktopMain) 75 | } 76 | 77 | val wasmJsMain by getting { 78 | dependsOn(desktopMain) 79 | } 80 | 81 | val jvmMain by getting { 82 | dependsOn(desktopMain) 83 | dependencies { 84 | implementation(compose.desktop.currentOs) 85 | } 86 | } 87 | } 88 | 89 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 90 | compilerOptions { 91 | freeCompilerArgs = listOf( 92 | "-opt-in=androidx.compose.animation.ExperimentalSharedTransitionApi", 93 | "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", 94 | "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", 95 | "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", 96 | "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", 97 | "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", 98 | "-opt-in=androidx.compose.material.ExperimentalMaterialApi", 99 | "-opt-in=kotlinx.coroutines.FlowPreview", 100 | "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", 101 | "-opt-in=com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi", 102 | "-Xexpect-actual-classes", 103 | ) 104 | } 105 | } 106 | 107 | group = "org.jetbrains.compose.storytale" 108 | 109 | publishing {} 110 | 111 | android { 112 | namespace = "org.jetbrains.compose.storytale.gallery" 113 | compileSdk = libs.versions.android.compileSdk.get().toInt() 114 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 115 | defaultConfig { 116 | minSdk = 24 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/AndroidSourceGeneratorTask.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.FileSpec 5 | import com.squareup.kotlinpoet.KModifier 6 | import java.io.File 7 | import java.nio.file.Files 8 | import org.gradle.api.DefaultTask 9 | import org.gradle.api.tasks.CacheableTask 10 | import org.gradle.api.tasks.Input 11 | import org.gradle.api.tasks.OutputDirectory 12 | import org.gradle.api.tasks.TaskAction 13 | 14 | @CacheableTask 15 | open class AndroidSourceGeneratorTask : DefaultTask() { 16 | @Input 17 | lateinit var title: String 18 | 19 | @Input 20 | lateinit var appPackageName: String 21 | 22 | @OutputDirectory 23 | lateinit var outputResourcesDir: File 24 | 25 | @OutputDirectory 26 | lateinit var outputSourcesDir: File 27 | 28 | @TaskAction 29 | fun generate() { 30 | cleanup(outputSourcesDir) 31 | cleanup(outputResourcesDir) 32 | 33 | generateSources() 34 | generateAndroidManifest() 35 | } 36 | 37 | private fun generateSources() { 38 | FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "MainViewController").apply { 39 | addImport("org.jetbrains.compose.storytale.gallery", "Gallery") 40 | 41 | function("MainViewController") { 42 | addAnnotation(ClassName("androidx.compose.runtime", "Composable")) 43 | addStatement("Gallery()") 44 | } 45 | } 46 | .build() 47 | .writeTo(outputSourcesDir) 48 | 49 | FileSpec.builder(appPackageName, "StorytaleAppActivity").apply { 50 | addImport("androidx.activity", "enableEdgeToEdge") 51 | addImport("androidx.activity.compose", "setContent") 52 | addImport(StorytaleGradlePlugin.STORYTALE_PACKAGE, "MainViewController") 53 | 54 | klass("StorytaleAppActivity") { 55 | superclass(ClassName("androidx.activity", "ComponentActivity")) 56 | function("onCreate") { 57 | addModifiers(KModifier.OVERRIDE) 58 | addParameter("savedInstanceState", ClassName("android.os", "Bundle").copy(nullable = true)) 59 | addStatement( 60 | """ 61 | | super.onCreate(savedInstanceState) 62 | | setContent { MainViewController() } 63 | """.trimMargin(), 64 | ) 65 | } 66 | } 67 | } 68 | .build() 69 | .writeTo(outputSourcesDir) 70 | } 71 | 72 | private fun generateAndroidManifest() { 73 | val androidManifestFile = File(outputResourcesDir, "AndroidManifest.xml") 74 | Files.createDirectories(androidManifestFile.parentFile.toPath()) 75 | androidManifestFile.writeText( 76 | """ 77 | 78 | 82 | 86 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | """.trimIndent(), 101 | ) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/Buttons.story.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("ktlint:standard:property-naming") 2 | 3 | package storytale.gallery.demo 4 | 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.Add 11 | import androidx.compose.material.icons.filled.AddCircle 12 | import androidx.compose.material3.Button 13 | import androidx.compose.material3.ElevatedButton 14 | import androidx.compose.material3.ExtendedFloatingActionButton 15 | import androidx.compose.material3.FilledTonalButton 16 | import androidx.compose.material3.FloatingActionButton 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.LargeFloatingActionButton 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.OutlinedButton 21 | import androidx.compose.material3.SegmentedButton 22 | import androidx.compose.material3.SegmentedButtonDefaults 23 | import androidx.compose.material3.SingleChoiceSegmentedButtonRow 24 | import androidx.compose.material3.SmallFloatingActionButton 25 | import androidx.compose.material3.Text 26 | import androidx.compose.material3.TextButton 27 | import androidx.compose.runtime.CompositionLocalProvider 28 | import androidx.compose.runtime.mutableIntStateOf 29 | import androidx.compose.runtime.remember 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.platform.LocalDensity 33 | import androidx.compose.ui.unit.dp 34 | import org.jetbrains.compose.storytale.story 35 | 36 | val `Floating Action Buttons` by story { 37 | val Density by parameter(LocalDensity.current) 38 | val `Container color` by parameter(MaterialTheme.colorScheme.primary) 39 | val bgColor = `Container color` 40 | 41 | CompositionLocalProvider(LocalDensity provides Density) { 42 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { 43 | SmallFloatingActionButton(onClick = {}, containerColor = bgColor) { 44 | Icon(imageVector = Icons.Default.Add, contentDescription = null) 45 | } 46 | FloatingActionButton(onClick = {}, containerColor = bgColor) { 47 | Icon(imageVector = Icons.Default.Add, contentDescription = null) 48 | } 49 | ExtendedFloatingActionButton(onClick = {}, containerColor = bgColor) { 50 | Icon(imageVector = Icons.Default.AddCircle, contentDescription = null) 51 | Spacer(Modifier.padding(4.dp)) 52 | Text("Extended") 53 | } 54 | LargeFloatingActionButton(onClick = {}, containerColor = bgColor) { 55 | Text("Large") 56 | } 57 | } 58 | } 59 | } 60 | 61 | val `Segmented buttons` by story { 62 | val selectedIndex = remember { mutableIntStateOf(0) } 63 | 64 | SingleChoiceSegmentedButtonRow { 65 | repeat(3) { index -> 66 | SegmentedButton( 67 | selected = index == selectedIndex.value, 68 | onClick = { selectedIndex.value = index }, 69 | shape = SegmentedButtonDefaults.itemShape(index, 3), 70 | ) { 71 | Text("Button $index", modifier = Modifier.padding(4.dp)) 72 | } 73 | } 74 | } 75 | } 76 | 77 | val `Common buttons` by story { 78 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { 79 | ElevatedButton(onClick = {}) { 80 | Text("Elevated Button") 81 | } 82 | 83 | Button(onClick = {}) { 84 | Text("Filled", softWrap = false) 85 | } 86 | 87 | FilledTonalButton(onClick = {}) { 88 | Text("Tonal", softWrap = false) 89 | } 90 | 91 | OutlinedButton(onClick = {}) { 92 | Text("Outlined", softWrap = false) 93 | } 94 | 95 | TextButton(onClick = {}) { 96 | Text("Text", softWrap = false) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /modules/preview-processor/src/main/kotlin/PreviewProcessor.kt: -------------------------------------------------------------------------------- 1 | import com.google.devtools.ksp.processing.CodeGenerator 2 | import com.google.devtools.ksp.processing.Dependencies 3 | import com.google.devtools.ksp.processing.KSPLogger 4 | import com.google.devtools.ksp.processing.Resolver 5 | import com.google.devtools.ksp.processing.SymbolProcessor 6 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 7 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 8 | import com.google.devtools.ksp.symbol.KSAnnotated 9 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 10 | import com.google.devtools.ksp.validate 11 | import com.squareup.kotlinpoet.ClassName 12 | import com.squareup.kotlinpoet.FileSpec 13 | import com.squareup.kotlinpoet.PropertySpec 14 | import com.squareup.kotlinpoet.buildCodeBlock 15 | import org.jetbrains.compose.storytale.plugin.StorytaleGradlePlugin 16 | 17 | class PreviewProcessor( 18 | private val logger: KSPLogger, 19 | private val codeGenerator: CodeGenerator, 20 | ) : SymbolProcessor { 21 | override fun process(resolver: Resolver): List { 22 | val (jetbrains, discarded1) = 23 | resolver.getSymbolsWithAnnotation("org.jetbrains.compose.ui.tooling.preview.Preview") 24 | .partition { it.validate() } 25 | val (androidxDesktop, discarded2) = resolver.getSymbolsWithAnnotation("androidx.compose.desktop.ui.tooling.preview.Preview") 26 | .partition { it.validate() } 27 | 28 | val (androidxAndroid, discarded3) = resolver.getSymbolsWithAnnotation("androidx.compose.ui.tooling.preview.Preview") 29 | .partition { it.validate() } 30 | 31 | val validPreviewFunctions = (jetbrains + androidxDesktop + androidxAndroid) 32 | .filter { it is KSFunctionDeclaration && it.validate() } 33 | .map { it as KSFunctionDeclaration } 34 | .sortedBy { (it.qualifiedName ?: it.simpleName).asString() } 35 | 36 | generatePreviewFile(validPreviewFunctions) 37 | 38 | return discarded1 + discarded2 + discarded3 39 | } 40 | 41 | private fun generatePreviewFile(previewFunctions: List) { 42 | if (previewFunctions.isEmpty()) return 43 | 44 | val packageName = StorytaleGradlePlugin.STORYTALE_PACKAGE 45 | val fileSpecBuilder = FileSpec.builder( 46 | packageName, 47 | "Previews.story", 48 | ).apply { 49 | indent(" ") 50 | addImport("org.jetbrains.compose.storytale", "story") 51 | 52 | previewFunctions.forEach { function -> 53 | addImport(function.packageName.asString(), function.simpleName.asString()) 54 | 55 | val functionName = function.simpleName.asString() 56 | val storyName = functionName.removePrefix("Preview").removeSuffix("Preview") 57 | 58 | addProperty( 59 | PropertySpec 60 | .builder( 61 | storyName.ifEmpty { functionName }, 62 | ClassName("org.jetbrains.compose.storytale", "Story"), 63 | ) 64 | .delegate( 65 | buildCodeBlock { 66 | beginControlFlow("story") 67 | addStatement("%N()", functionName) 68 | endControlFlow() 69 | }, 70 | ) 71 | .build(), 72 | ) 73 | } 74 | } 75 | 76 | val containingFiles = previewFunctions 77 | .mapNotNull { it.containingFile } 78 | .distinct() 79 | 80 | val dependencies = if (containingFiles.isNotEmpty()) { 81 | Dependencies(true, containingFiles.first()) 82 | } else { 83 | Dependencies.ALL_FILES 84 | } 85 | 86 | val file = codeGenerator.createNewFile( 87 | dependencies, 88 | packageName, 89 | "Previews.story", 90 | ) 91 | fileSpecBuilder.build().toJavaFileObject().openInputStream().copyTo(file) 92 | } 93 | 94 | class Provider : SymbolProcessorProvider { 95 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { 96 | return PreviewProcessor( 97 | environment.logger, 98 | environment.codeGenerator, 99 | ) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/src/commonMain/composeResources/drawable/compose-multiplatform.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 24 | 30 | 36 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.11.0" 3 | android-targetSdk = "36" 4 | android-compileSdk = "36" 5 | android-minSdk = "26" 6 | androidx-activityCompose = "1.9.3" 7 | assertj = "3.27.2" 8 | kotlinCompileTesting = "0.7.0" 9 | junitVersion = "4.13.2" 10 | storytale = "0.0.3+dev8" 11 | 12 | compose-plugin = "1.8.1" 13 | compose-hot-reload = "1.0.0-beta06" 14 | compose-navigation = "2.9.0-alpha16" 15 | kotlinx-serialization-json = "1.7.3" 16 | kotlin = "2.1.21" 17 | kotlin-poet = "1.18.1" 18 | build-time-config = "2.3.0" 19 | code-highlights = "1.0.0" 20 | spotless = "7.0.2" 21 | dokka = "1.9.10" 22 | kotlinx-html = "0.7.3" 23 | junit = "5.10.1" 24 | jsoup = "1.16.1" 25 | ktlint = "1.5.0" 26 | composeRules = "0.4.22" 27 | material3-icons = "1.7.3" 28 | material3-adaptive = "1.1.0-beta01" 29 | ksp = "2.1.20-2.0.1" 30 | 31 | [libraries] 32 | assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } 33 | kotlinCompileTesting-core = { module = "dev.zacsweers.kctfork:core", version.ref = "kotlinCompileTesting" } 34 | kotlinCompileTesting-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "kotlinCompileTesting" } 35 | junit = { module = "junit:junit", version.ref = "junitVersion" } 36 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 37 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 38 | kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 39 | compose-gradle-plugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-plugin" } 40 | compose-hot-reload-plugin = { module = "org.jetbrains.compose.hot-reload:hot-reload-gradle-plugin", version.ref = "compose-hot-reload" } 41 | kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } 42 | kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "kotlin-poet" } 43 | android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } 44 | navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "compose-navigation" } 45 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } 46 | compose-highlights = { module = "dev.snipme:highlights", version.ref = "code-highlights" } 47 | dokka-core = { module = "org.jetbrains.dokka:dokka-core", version.ref = "dokka" } 48 | dokka-base = { module = "org.jetbrains.dokka:dokka-base", version.ref = "dokka" } 49 | dokka-test-api = { module = "org.jetbrains.dokka:dokka-test-api", version.ref = "dokka" } 50 | dokka-base-test-utils = { module = "org.jetbrains.dokka:dokka-base-test-utils", version.ref = "dokka" } 51 | kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.ref = "kotlinx-html" } 52 | junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } 53 | jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } 54 | ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" } 55 | composeRules = { module = "io.nlopez.compose.rules:ktlint", version.ref = "composeRules" } 56 | material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "material3-adaptive" } 57 | material3-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "material3-icons" } 58 | ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } 59 | 60 | [plugins] 61 | androidApplication = { id = "com.android.application", version.ref = "agp" } 62 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 63 | serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 64 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 65 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 66 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 67 | compose-hot-reload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose-hot-reload" } 68 | buildTimeConfig = { id = "dev.limebeck.build-time-config", version.ref = "build-time-config" } 69 | storytale = { id = "org.jetbrains.compose.storytale", version.ref = "storytale" } 70 | kotlinDsl = { id = "kotlin-dsl" } 71 | spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } 72 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 73 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 74 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 75 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/WasmSourceGeneratorTask.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import com.squareup.kotlinpoet.AnnotationSpec 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.FileSpec 6 | import java.io.File 7 | import org.gradle.api.DefaultTask 8 | import org.gradle.api.tasks.CacheableTask 9 | import org.gradle.api.tasks.Input 10 | import org.gradle.api.tasks.OutputDirectory 11 | import org.gradle.api.tasks.TaskAction 12 | import org.jetbrains.kotlin.incremental.createDirectory 13 | 14 | @CacheableTask 15 | open class WasmSourceGeneratorTask : DefaultTask() { 16 | @Input 17 | lateinit var title: String 18 | 19 | @OutputDirectory 20 | lateinit var outputResourcesDir: File 21 | 22 | @OutputDirectory 23 | lateinit var outputSourcesDir: File 24 | 25 | @TaskAction 26 | fun generate() { 27 | cleanup(outputSourcesDir) 28 | cleanup(outputResourcesDir) 29 | 30 | generateSources() 31 | generateResources() 32 | } 33 | 34 | private fun generateSources() { 35 | val file = FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "Main").apply { 36 | addMainFileImports() 37 | addMainViewControllerFun() 38 | function("main") { 39 | addStatement("MainViewController()") 40 | } 41 | }.build() 42 | 43 | file.writeTo(outputSourcesDir) 44 | } 45 | 46 | private fun generateResources() { 47 | if (!outputResourcesDir.exists()) { 48 | outputResourcesDir.createDirectory() 49 | } 50 | 51 | val styles = File(outputResourcesDir, "styles.css") 52 | styles.writeText(webStylesCssContent) 53 | 54 | val index = File(outputResourcesDir, "index.html") 55 | index.writeText( 56 | webIndexHtmlContent( 57 | jsFileName = JsSourceGeneratorTask.SCRIPT_FILE_NAME, 58 | addSkikoJs = false, 59 | ), 60 | ) 61 | } 62 | } 63 | 64 | internal fun FileSpec.Builder.addMainFileImports() { 65 | addImport("androidx.compose.ui.window", "ComposeViewport") 66 | addImport("kotlinx.browser", "document") 67 | addImport("kotlinx.browser", "window") 68 | addImport("org.jetbrains.compose.storytale.gallery", "Gallery") 69 | addImport("org.jetbrains.compose.storytale.gallery.story.code", "JetBrainsMonoRegularRes") 70 | addImport("org.jetbrains.compose.resources", "preloadFont") 71 | } 72 | 73 | internal fun FileSpec.Builder.addMainViewControllerFun() { 74 | val optInExperimentalComposeUi = AnnotationSpec.builder(ClassName("kotlin", "OptIn")).addMember( 75 | "androidx.compose.ui.ExperimentalComposeUiApi::class, org.jetbrains.compose.resources.ExperimentalResourceApi::class", 76 | ).build() 77 | 78 | function("MainViewController") { 79 | addAnnotation(optInExperimentalComposeUi) 80 | addStatement( 81 | """ 82 | |val useEmbedded = window.location.search.contains("embedded=true") 83 | | 84 | |ComposeViewport(document.body!!) { 85 | | val hasResourcePreloadCompleted = preloadFont(JetBrainsMonoRegularRes).value != null 86 | | 87 | | if (hasResourcePreloadCompleted) { 88 | | Gallery(isEmbedded = useEmbedded) 89 | | } 90 | | 91 | |} 92 | | 93 | """.trimMargin(), 94 | ) 95 | } 96 | } 97 | 98 | internal val webStylesCssContent = """ 99 | |html, body { 100 | | width: 100%; 101 | | height: 100%; 102 | | margin: 0; 103 | | padding: 0; 104 | | overflow: hidden; 105 | |} 106 | """.trimMargin() 107 | 108 | internal fun webIndexHtmlContent(jsFileName: String, addSkikoJs: Boolean = true): String { 109 | return """ 110 | | 111 | | 112 | | 113 | | 114 | | 115 | | Gallery 116 | | 117 | | ${if (addSkikoJs) """""" else ""} 118 | | 119 | | 120 | | 121 | | 122 | | 123 | """.trimMargin() 124 | } 125 | -------------------------------------------------------------------------------- /examples/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig 5 | 6 | plugins { 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.storytale) 9 | alias(libs.plugins.kotlinMultiplatform) 10 | alias(libs.plugins.androidApplication) 11 | alias(libs.plugins.jetbrainsCompose) 12 | alias(libs.plugins.compose.hot.reload) 13 | } 14 | 15 | configurations.all { 16 | resolutionStrategy.dependencySubstitution { 17 | substitute(module("org.jetbrains.compose.storytale:compiler-plugin")) 18 | .using(project(":modules:compiler-plugin")) 19 | substitute(module("org.jetbrains.compose.storytale:runtime-api")) 20 | .using(project(":modules:runtime-api")) 21 | substitute(module("org.jetbrains.compose.storytale:gallery")) 22 | .using(project(":modules:gallery")) 23 | } 24 | } 25 | 26 | kotlin { 27 | wasmJs { 28 | moduleName = "composeApp" 29 | browser { 30 | commonWebpackConfig { 31 | outputFileName = "composeApp.js" 32 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 33 | static = (static ?: mutableListOf()).apply { 34 | // Serve sources to debug inside browser 35 | add(project.projectDir.path) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | androidTarget { 43 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 44 | compilerOptions { 45 | jvmTarget.set(JvmTarget.JVM_11) 46 | } 47 | } 48 | 49 | jvm("desktop") 50 | 51 | listOf( 52 | iosX64(), 53 | iosArm64(), 54 | iosSimulatorArm64(), 55 | ).forEach { iosTarget -> 56 | iosTarget.binaries.framework { 57 | baseName = "ComposeApp" 58 | isStatic = true 59 | binaryOption("bundleId", "Storytale Examples App") 60 | } 61 | } 62 | 63 | applyDefaultHierarchyTemplate() 64 | 65 | sourceSets { 66 | val desktopMain by getting 67 | 68 | androidMain.dependencies { 69 | implementation(compose.preview) 70 | implementation(libs.androidx.activity.compose) 71 | } 72 | commonMain.dependencies { 73 | implementation(compose.runtime) 74 | implementation(compose.foundation) 75 | implementation(compose.material) 76 | implementation(compose.ui) 77 | implementation(compose.components.resources) 78 | implementation(compose.components.uiToolingPreview) 79 | } 80 | desktopMain.dependencies { 81 | implementation(compose.desktop.currentOs) 82 | } 83 | } 84 | } 85 | 86 | android { 87 | namespace = "org.jetbrains.compose.storytale.example" 88 | compileSdk = libs.versions.android.compileSdk.get().toInt() 89 | 90 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 91 | sourceSets["main"].res.srcDirs("src/androidMain/res") 92 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 93 | 94 | defaultConfig { 95 | applicationId = "org.jetbrains.compose.storytale.example" 96 | minSdk = libs.versions.android.minSdk.get().toInt() 97 | targetSdk = libs.versions.android.targetSdk.get().toInt() 98 | versionCode = 1 99 | versionName = "1.0" 100 | } 101 | packaging { 102 | resources { 103 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 104 | } 105 | } 106 | buildTypes { 107 | getByName("release") { 108 | isMinifyEnabled = false 109 | } 110 | } 111 | compileOptions { 112 | sourceCompatibility = JavaVersion.VERSION_11 113 | targetCompatibility = JavaVersion.VERSION_11 114 | } 115 | buildFeatures { 116 | compose = true 117 | } 118 | dependencies { 119 | debugImplementation(compose.uiTooling) 120 | } 121 | } 122 | 123 | compose.resources { 124 | publicResClass = true 125 | packageOfResClass = "org.jetbrains.compose.storytale.example" 126 | } 127 | 128 | compose.desktop { 129 | application { 130 | mainClass = "MainKt" 131 | 132 | nativeDistributions { 133 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 134 | packageName = "org.jetbrains.compose.storytale" 135 | packageVersion = "1.0.0" 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/EmbeddedStoryView.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.gallery.material3 2 | 3 | import androidx.compose.animation.EnterTransition 4 | import androidx.compose.animation.ExitTransition 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material3.CenterAlignedTopAppBar 13 | import androidx.compose.material3.HorizontalDivider 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.IconButton 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.platform.LocalUriHandler 22 | import androidx.compose.ui.platform.UriHandler 23 | import androidx.compose.ui.text.SpanStyle 24 | import androidx.compose.ui.text.buildAnnotatedString 25 | import androidx.compose.ui.text.style.TextDecoration 26 | import androidx.compose.ui.text.withStyle 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.unit.sp 29 | import androidx.navigation.NavHostController 30 | import androidx.navigation.compose.NavHost 31 | import androidx.navigation.compose.composable 32 | import androidx.navigation.toRoute 33 | import org.jetbrains.compose.storytale.storiesStorage 34 | 35 | @Composable 36 | fun EmbeddedStoryView( 37 | appState: StorytaleGalleryAppState, 38 | navHostController: NavHostController, 39 | ) { 40 | NavHost( 41 | navController = navHostController, 42 | startDestination = StoryScreen(""), 43 | enterTransition = { EnterTransition.None }, 44 | exitTransition = { ExitTransition.None }, 45 | popEnterTransition = { EnterTransition.None }, 46 | popExitTransition = { ExitTransition.None }, 47 | ) { 48 | composable { 49 | val args = it.toRoute() 50 | val storyName = args.storyName 51 | Column( 52 | modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface), 53 | horizontalAlignment = Alignment.CenterHorizontally, 54 | ) { 55 | CenterAlignedTopAppBar( 56 | title = { 57 | Text( 58 | text = storyName, 59 | style = MaterialTheme.typography.titleMedium, 60 | modifier = Modifier.padding(12.dp), 61 | ) 62 | }, 63 | actions = { 64 | ThemeSwitcherIconButton(appState) 65 | 66 | val uriHandler = LocalUriHandler.current 67 | 68 | IconButton(onClick = { 69 | openFullScreenStory(args, uriHandler) 70 | }) { 71 | Icon(imageVector = OpenInFull, contentDescription = null) 72 | } 73 | }, 74 | ) 75 | StoryContent( 76 | activeStory = storiesStorage.firstOrNull { 77 | it.name == storyName 78 | }, 79 | useEmbeddedView = true, 80 | modifier = Modifier.weight(1f), 81 | ) 82 | HorizontalDivider() 83 | Box(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 12.dp)) { 84 | val uriHandler = LocalUriHandler.current 85 | Text( 86 | text = buildAnnotatedString { 87 | append("Powered by ") 88 | withStyle( 89 | style = SpanStyle( 90 | textDecoration = TextDecoration.Underline, 91 | color = MaterialTheme.colorScheme.primary, 92 | ), 93 | ) { 94 | append("Storytale") 95 | } 96 | }, 97 | modifier = Modifier.align(Alignment.CenterEnd).clickable(onClick = { 98 | uriHandler.openUri("https://github.com/Kotlin/Storytale") 99 | }), 100 | style = MaterialTheme.typography.bodySmall, 101 | fontSize = 9.sp, 102 | color = MaterialTheme.colorScheme.onSurfaceVariant, 103 | ) 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * EXPERIMENTAL! 112 | * A lambda to be provided by the Storytale gallery consumers to open a full screen gallery. 113 | */ 114 | var openFullScreenStory: (StoryScreen, UriHandler) -> Unit = { story, uriHandler -> 115 | println("openFullScreenStory($story) is not implemented. It should be provided by the Gallery consumer.") 116 | } 117 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/JsMultiplatformTasks.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") 2 | 3 | package org.jetbrains.compose.storytale.plugin 4 | 5 | import org.gradle.api.Project 6 | import org.gradle.api.file.DuplicatesStrategy 7 | import org.gradle.api.tasks.AbstractCopyTask 8 | import org.gradle.kotlin.dsl.task 9 | import org.gradle.kotlin.dsl.withType 10 | import org.jetbrains.compose.web.tasks.UnpackSkikoWasmRuntimeTask 11 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation 12 | import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.ResolveResourcesFromDependenciesTask 13 | import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl 14 | import org.jetbrains.kotlin.gradle.targets.js.ir.JsIrBinary 15 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinBrowserJsIr 16 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrCompilation 17 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget 18 | import org.jetbrains.kotlin.gradle.targets.js.ir.WebpackConfigurator 19 | import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile 20 | 21 | fun Project.processJsCompilation(extension: StorytaleExtension, target: KotlinJsIrTarget) { 22 | project.logger.info("Configuring storytale for Kotlin/JS") 23 | createJsStorytaleGenerateSourceTask(extension, target) 24 | 25 | val storytaleCompilation = createWasmAndJsStorytaleCompilation(extension, target) 26 | 27 | createWasmAndJsStorytaleExecTask(storytaleCompilation) 28 | } 29 | 30 | fun Project.createWasmAndJsStorytaleCompilation( 31 | extension: StorytaleExtension, 32 | target: KotlinJsIrTarget, 33 | ): KotlinJsIrCompilation { 34 | val mainCompilation = target.compilations.named(KotlinCompilation.MAIN_COMPILATION_NAME).get() 35 | val storytaleBuildDir = extension.getBuildDirectory(target) 36 | val storytaleCompilation = 37 | target.compilations.create(StorytaleGradlePlugin.STORYTALE_SOURCESET_SUFFIX) as KotlinJsIrCompilation 38 | 39 | storytaleCompilation.associateWith(mainCompilation) 40 | setupResourceResolvingForTarget(storytaleBuildDir, storytaleCompilation) 41 | 42 | (mainCompilation.target as KotlinJsTargetDsl).apply { 43 | // force to create executable: required for IR, do nothing on Legacy 44 | binaries.executable(storytaleCompilation) 45 | } 46 | 47 | project.afterEvaluate { 48 | storytaleCompilation.apply { 49 | val sourceSet = kotlinSourceSets.single() 50 | 51 | sourceSet.dependsOn(extension.mainStoriesSourceSet) 52 | 53 | sourceSet.kotlin.setSrcDirs(files("$storytaleBuildDir/sources")) 54 | 55 | val generateTaskName = "${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}" 56 | 57 | val unpackSkikoTask = extension.project.tasks.withType().single() 58 | val resolveDependencyResourcesTask = extension.project.tasks 59 | .getByName(storytaleCompilation.resolveDependencyResourcesTaskName) as ResolveResourcesFromDependenciesTask 60 | 61 | sourceSet.resources.srcDirs( 62 | "$storytaleBuildDir/resources", 63 | unpackSkikoTask.outputDir, 64 | resolveDependencyResourcesTask.outputDirectory, 65 | mainCompilation.defaultSourceSet.resources, 66 | ) 67 | 68 | extension.project.tasks.named(processResourcesTaskName).configure { 69 | dependsOn(unpackSkikoTask) 70 | dependsOn(generateTaskName) 71 | dependsOn(resolveDependencyResourcesTask) 72 | 73 | (this as? AbstractCopyTask)?.duplicatesStrategy = DuplicatesStrategy.INCLUDE 74 | } 75 | 76 | compileTaskProvider.configure { 77 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP 78 | description = "Compile web storytale source files for '${target.name}'" 79 | 80 | dependsOn(generateTaskName) 81 | configureOptions() 82 | } 83 | 84 | binaries.withType(JsIrBinary::class.java) 85 | .configureEach { linkTask.configure { configureOptions() } } 86 | } 87 | } 88 | 89 | return storytaleCompilation 90 | } 91 | 92 | private fun Kotlin2JsCompile.configureOptions() { 93 | compilerOptions.sourceMap.set(true) 94 | } 95 | 96 | private fun Project.createJsStorytaleGenerateSourceTask(extension: StorytaleExtension, target: KotlinJsIrTarget) { 97 | val storytaleBuildDir = extension.getBuildDirectory(target) 98 | task("${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}") { 99 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP 100 | description = "Generate JS source files for '${target.name}'" 101 | title = target.name 102 | outputResourcesDir = file("$storytaleBuildDir/resources") 103 | outputSourcesDir = file("$storytaleBuildDir/sources") 104 | } 105 | } 106 | 107 | fun Project.createWasmAndJsStorytaleExecTask(compilation: KotlinJsIrCompilation) { 108 | afterEvaluate { 109 | val browser = compilation.target.browser as KotlinBrowserJsIr 110 | 111 | browser.subTargetConfigurators.withType().configureEach { 112 | setupBuild(compilation) 113 | setupRun(compilation) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/Utils.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi::class) 2 | @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") 3 | 4 | package org.jetbrains.compose.storytale.plugin 5 | 6 | import com.squareup.kotlinpoet.FileSpec 7 | import com.squareup.kotlinpoet.FunSpec 8 | import com.squareup.kotlinpoet.MemberSpecHolder 9 | import com.squareup.kotlinpoet.TypeSpec 10 | import java.io.ByteArrayOutputStream 11 | import java.io.File 12 | import java.io.IOException 13 | import java.io.InputStream 14 | import java.nio.file.Files 15 | import java.nio.file.StandardCopyOption 16 | import java.util.zip.ZipInputStream 17 | import org.gradle.api.Action 18 | import org.gradle.api.DefaultTask 19 | import org.gradle.api.Project 20 | import org.gradle.api.file.DirectoryProperty 21 | import org.gradle.api.file.FileCollection 22 | import org.gradle.api.provider.Property 23 | import org.gradle.api.tasks.Input 24 | import org.gradle.api.tasks.OutputDirectory 25 | import org.gradle.api.tasks.TaskAction 26 | import org.gradle.configurationcache.extensions.capitalized 27 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation 28 | import org.jetbrains.kotlin.gradle.plugin.KotlinTarget 29 | import org.jetbrains.kotlin.gradle.plugin.mpp.internal 30 | import org.jetbrains.kotlin.gradle.plugin.mpp.publishing.configureResourcesPublicationAttributes 31 | import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.ResolveResourcesFromDependenciesTask 32 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget 33 | 34 | fun cleanup(file: File) { 35 | if (file.exists()) { 36 | val listing = file.listFiles() 37 | if (listing != null) { 38 | for (sub in listing) { 39 | cleanup(sub) 40 | } 41 | } 42 | file.delete() 43 | } 44 | } 45 | 46 | inline fun MemberSpecHolder.Builder<*>.function( 47 | name: String, 48 | builderAction: FunSpec.Builder.() -> Unit, 49 | ): FunSpec { 50 | return FunSpec.builder(name).apply(builderAction).build().also { 51 | addFunction(it) 52 | } 53 | } 54 | 55 | inline fun FileSpec.Builder.klass( 56 | name: String, 57 | builderAction: TypeSpec.Builder.() -> Unit, 58 | ): TypeSpec { 59 | return TypeSpec.classBuilder(name).apply(builderAction).build().also { 60 | addType(it) 61 | } 62 | } 63 | 64 | fun StorytaleExtension.getBuildDirectory(target: KotlinTarget) = with(project) { 65 | file(buildDir.resolve(this@getBuildDirectory.buildDir).resolve(name).resolve(target.name)) 66 | } 67 | 68 | abstract class UnzipResourceTask : DefaultTask() { 69 | @get:Input 70 | abstract val resourcePath: Property 71 | 72 | @get:OutputDirectory 73 | abstract val outputDir: DirectoryProperty 74 | 75 | @TaskAction 76 | fun unzip() { 77 | val resourcePath = resourcePath.get() 78 | javaClass.classLoader.getResourceAsStream(resourcePath)?.use { zipStream -> 79 | unzipStream(zipStream) 80 | } ?: throw IOException("Resource not found: $resourcePath") 81 | } 82 | 83 | private fun unzipStream(zipStream: InputStream) { 84 | val outputDir = outputDir.get() 85 | 86 | ZipInputStream(zipStream).use { zis -> 87 | while (true) { 88 | val entry = zis.nextEntry ?: break 89 | val newFile = File(outputDir.asFile, entry.name) 90 | if (entry.isDirectory) { 91 | newFile.mkdirs() 92 | } else { 93 | newFile.parentFile.mkdirs() 94 | Files.copy(zis, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING) 95 | } 96 | zis.closeEntry() 97 | } 98 | } 99 | } 100 | } 101 | 102 | val KotlinCompilation<*>.resolveDependencyResourcesTaskName: String 103 | get() = "${name.lowercase()}${target.name.capitalized()}ResolveDependencyResources" 104 | 105 | fun setupResourceResolvingForTarget(storytaleBuildDir: File, compilation: KotlinCompilation<*>) { 106 | compilation.project.tasks.register( 107 | compilation.resolveDependencyResourcesTaskName, 108 | ResolveResourcesFromDependenciesTask::class.java, 109 | Action { 110 | filterResourcesByExtension.set(true) 111 | archivesFromDependencies.setFrom(getArchivesFromResources(compilation)) 112 | outputDirectory.set(storytaleBuildDir.resolve("resolved-dependency-resources")) 113 | }, 114 | ) 115 | } 116 | 117 | fun getArchivesFromResources(compilation: KotlinCompilation<*>): FileCollection { 118 | val dependenciesConfiguration = if (compilation.target is KotlinJsIrTarget) { 119 | compilation.internal.configurations.runtimeDependencyConfiguration 120 | ?: return compilation.project.files() 121 | } else { 122 | compilation.internal.configurations.compileDependencyConfiguration 123 | } 124 | 125 | return dependenciesConfiguration.incoming.artifactView { 126 | withVariantReselection() 127 | attributes { 128 | configureResourcesPublicationAttributes(compilation.target) 129 | } 130 | isLenient = true 131 | }.files 132 | } 133 | 134 | fun Project.execute(vararg args: String): String = ByteArrayOutputStream().apply { 135 | exec { 136 | commandLine(*args) 137 | standardOutput = this@apply 138 | } 139 | }.toString() 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Frame 482360](https://github.com/user-attachments/assets/b90b5776-f2f4-4385-8b7d-94eb912eacdf) 2 | 3 | [![Incubator](https://jb.gg/badges/incubator-plastic.svg)](https://github.com/JetBrains#jetbrains-on-github) 4 | 5 | # Storytale 6 | 7 | Storytale is a Gradle Plugin designed to help developers to show their composables and develop them isolated by generating a gallery of the project components. 8 | Check the `examples` and their generated web gallery [here](https://kotlin.github.io/Storytale) 9 | 10 | Since Storytale is still in the early stages of development, the api is marked as unstable, but this section will also show you how to use `Storytale` to write code for your components, so let's get started! 🌟 11 | 12 | All platforms 13 | 14 | ## ⚙️ Getting Started 15 | 16 | ### 1. Setup 17 | 18 | #### Import Dependencies 19 | 20 |
21 | using Version Catalog 22 | 23 | > **libs.versions.toml** 24 | 25 | ```toml 26 | [versions] 27 | storytale = "0.0.4-alpha01+dev19" 28 | 29 | [plugins] 30 | storytale = { id = "org.jetbrains.compose.storytale", version.ref = "storytale" } 31 | ``` 32 | 33 | For the latest version check out the [Releases page](https://github.com/Kotlin/Storytale/releases) 34 | 35 | > **build.gradle.kts** `root level` 36 | ```kotlin 37 | plugins { 38 | alias(libs.plugins.storytale) apply false 39 | } 40 | ``` 41 |
42 | 43 | > **build.gradle.kts** `app level` 44 | ```kotlin 45 | plugins { 46 | alias(libs.plugins.storytale) 47 | } 48 | ``` 49 | 50 | 51 | ```kotlin 52 | repositories { 53 | mavenCentral() 54 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 55 | } 56 | ``` 57 | 58 | > [!NOTE] 59 | > Storytale **has not** yet released its first version on `mavenCentral`. Currently, we publish dev builds to maven("https://maven.pkg.jetbrains.space/public/p/compose/dev"), so it's required to add this repository as shown above. 60 | 61 | ### 2. Create Sourcesets for Storytale on the target platform (for multi-platform projects) 62 | 63 | Storytale can be used for Compose Multiplatform projects. To start using the Storytale API, you need to define a sourceset for the component you want to test (for example, it might only be used for `Android/iOS` platforms, or it could be common for all platforms). 64 | 65 | In your app's 'src' folder, go to New -> Directory: 66 | 67 | image 68 | 69 | ### 3. Usage 70 | 71 | Now, your project structure will look like this: 72 | 73 | ``` 74 | └── src/ 75 | ├── androidMain 76 | ├── commonMain 77 | ├── xxxxxStories/ 78 | │ └── kotlin 79 | └── desktopMain 80 | ``` 81 | 82 | Let's try to write a simple function in `commonMain`: 83 | 84 | `commonMain/PrimaryButton.kt` 85 | ```kotlin 86 | @Composable 87 | fun PrimaryButton(onClick: () -> Unit, enabled: Boolean = true) { 88 | Button(onClick = onClick, enabled = enabled) { 89 | Text("Click me!") 90 | } 91 | } 92 | ``` 93 | 94 | `commonStories/kotlin/PrimaryButton.story.kt` 95 | ```kotlin 96 | import org.jetbrains.compose.storytale.story 97 | 98 | val `PrimaryButton default state` by story { 99 | val enabled by parameter(true) 100 | PrimaryButton(onClick = {}, enabled = enabled) 101 | } 102 | ``` 103 | 104 | Next, let's run the `desktopStoriesRun` command, you can find it in the `project/Storytale` section on the right side of the Gradle panel. 105 | 106 | image 107 | 108 | If you can't find all Gradle tasks containing `Storytale` after syncing, check if this option is enabled: 109 | 110 | `settings->Experimental` 111 | 112 | image 113 | 114 | 115 | ## Building and Contributing 116 | 117 | Once the sync is successful, run `./gradlew publishToMavenLocal`. 118 | 119 | At this point, if you see these Storytale `Gradle tasks`, it means you’ve successfully set up the project and can start contributing! :) 120 | 121 | image 122 | 123 | Before running `XXXXStoriesRun`, you need to run `./gradlew publishToMavenLocal` to deploy the latest changes if you've modified any part of the code (except for examples module) 124 | 125 | #### About project structure 126 | 127 | ``` 128 | . 129 | └── modules/ 130 | ├── compiler-plugin 131 | ├── gallery 132 | ├── gradle-plugin 133 | └── runtime 134 | ``` 135 | 136 | ##### compiler-plugin 137 | 138 | Includes the entry point of the Storytale compiler plugin and its related features. 139 | 140 | ##### gallery 141 | 142 | The gallery represents the final, fully functional multi-platform application that is produced by Storytale. 143 | 144 | ##### gradle-plugin 145 | 146 | All aspects related to building Storytale, including various Gradle tasks, generating Storytale apps for different platforms, and so on. 147 | 148 | #### runtime 149 | 150 | The runtime module is designed to provide developers with essential APIs during the coding process 151 | 152 | ## Feedback and questions 153 | 154 | Share your feedback or questions in our [#storytale](https://slack-chats.kotlinlang.org/c/storytale) Slack channel. 155 | [Get a Slack invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up). 156 | -------------------------------------------------------------------------------- /modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/JvmMultiplatformTasks.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.compose.storytale.plugin 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.file.FileCollection 5 | import org.gradle.api.plugins.JavaPluginExtension 6 | import org.gradle.api.provider.Provider 7 | import org.gradle.api.tasks.JavaExec 8 | import org.gradle.jvm.toolchain.JavaLauncher 9 | import org.gradle.jvm.toolchain.JavaToolchainService 10 | import org.gradle.kotlin.dsl.register 11 | import org.gradle.kotlin.dsl.task 12 | import org.jetbrains.compose.reload.gradle.ComposeHotRun 13 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation 14 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation 15 | import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget 16 | 17 | fun Project.processJvmCompilation(extension: StorytaleExtension, target: KotlinJvmTarget) { 18 | project.logger.info("Configuring storytale for Kotlin/JVM") 19 | createJvmStorytaleGenerateSourceTask(extension, target) 20 | 21 | val storytaleCompilation = createJvmStorytaleCompileTask(extension, target) 22 | 23 | val runtimeClasspath = storytaleCompilation.output.allOutputs + storytaleCompilation.runtimeDependencyFiles 24 | createJvmStorytaleExecTask(extension, storytaleCompilation, target, runtimeClasspath) 25 | createJvmStorytaleHotReloadExecTask(compilation = storytaleCompilation, target = target) 26 | } 27 | 28 | private fun Project.createJvmStorytaleGenerateSourceTask( 29 | extension: StorytaleExtension, 30 | target: KotlinJvmTarget, 31 | ) { 32 | val storytaleBuildDir = extension.getBuildDirectory(target) 33 | task("${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}") { 34 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP 35 | description = "Generate JVM source files for '${target.name}'" 36 | title = target.name 37 | outputResourcesDir = file("$storytaleBuildDir/resources") 38 | outputSourcesDir = file("$storytaleBuildDir/sources") 39 | } 40 | } 41 | 42 | fun Project.createJvmStorytaleCompileTask( 43 | extension: StorytaleExtension, 44 | target: KotlinJvmTarget, 45 | ): KotlinJvmCompilation { 46 | val mainCompilation = target.compilations.named(KotlinCompilation.MAIN_COMPILATION_NAME).get() 47 | val storytaleBuildDir = extension.getBuildDirectory(target) 48 | val storytaleCompilation = 49 | target.compilations.create(StorytaleGradlePlugin.STORYTALE_SOURCESET_SUFFIX) as KotlinJvmCompilation 50 | 51 | storytaleCompilation.associateWith(mainCompilation) 52 | 53 | storytaleCompilation.apply { 54 | val sourceSet = kotlinSourceSets.single() 55 | 56 | sourceSet.dependsOn(extension.mainStoriesSourceSet) 57 | 58 | sourceSet.kotlin.setSrcDirs(files("$storytaleBuildDir/sources")) 59 | 60 | val generateTaskName = "${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}" 61 | 62 | sourceSet.resources.srcDirs("$storytaleBuildDir/resources", mainCompilation.defaultSourceSet.resources) 63 | 64 | extension.project.tasks.named(processResourcesTaskName).configure { 65 | dependsOn(generateTaskName) 66 | } 67 | 68 | compileTaskProvider.configure { 69 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP 70 | description = "Compile JVM storytale source files for '${target.name}'" 71 | 72 | dependsOn(generateTaskName) 73 | } 74 | } 75 | return storytaleCompilation 76 | } 77 | 78 | private fun Project.createJvmStorytaleExecTask( 79 | extension: StorytaleExtension, 80 | compilation: KotlinJvmCompilation, 81 | target: KotlinJvmTarget, 82 | runtimeClasspath: FileCollection, 83 | ) { 84 | task("${target.name}${StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX}Run") { 85 | dependsOn(compilation.compileTaskProvider) 86 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP 87 | description = "Execute storytale for '${target.name}'" 88 | 89 | val storiesBuildDir = extension.getBuildDirectory(target) 90 | mainClass.set("org.jetbrains.compose.storytale.generated.MainKt") 91 | 92 | classpath( 93 | file("$storiesBuildDir/classes"), 94 | file("$storiesBuildDir/resources"), 95 | runtimeClasspath, 96 | ) 97 | 98 | javaLauncher.set(javaLauncherProvider()) 99 | } 100 | } 101 | 102 | private fun Project.createJvmStorytaleHotReloadExecTask( 103 | compilation: KotlinJvmCompilation, 104 | target: KotlinJvmTarget, 105 | ) { 106 | val taskName = "${target.name}${StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX}HotRun" 107 | project.plugins.withId("org.jetbrains.compose.hot-reload") { 108 | logger.info("Compose Hot Reload plugin found, configuring Storytale Hot Reload for ${target.name}") 109 | tasks.register(taskName) { 110 | this.compilation.set(compilation) 111 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP 112 | description = "Execute storytale for '${target.name}' with hot reload" 113 | mainClass.set("org.jetbrains.compose.storytale.generated.MainKt") 114 | } 115 | } 116 | } 117 | 118 | private fun Project.javaLauncherProvider(): Provider = provider { 119 | val toolchainService = extensions.findByType(JavaToolchainService::class.java) ?: return@provider null 120 | val javaExtension = extensions.findByType(JavaPluginExtension::class.java) ?: return@provider null 121 | toolchainService.launcherFor(javaExtension.toolchain).orNull 122 | } 123 | -------------------------------------------------------------------------------- /modules/preview-processor-test/src/androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt: -------------------------------------------------------------------------------- 1 | package com.storytale 2 | 3 | import com.tschuchort.compiletesting.KotlinCompilation 4 | import java.lang.reflect.Modifier 5 | import kotlin.test.Ignore 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.junit.Test 8 | import util.storytaleTest 9 | 10 | class MakePreviewPublicFirExtensionRegistrarAndroidTest { 11 | 12 | @Test 13 | fun `ensure that private androidx Preview functions are made public`() { 14 | val compilation = storytaleTest { 15 | "PrivateAndroidPreview.kt" hasContent """ 16 | package test 17 | 18 | import androidx.compose.runtime.Composable 19 | 20 | @Composable 21 | private fun PrivateAndroidComponent() {} 22 | 23 | @androidx.compose.ui.tooling.preview.Preview 24 | @Composable 25 | private fun PrivateAndroidPreviewFunction() { 26 | PrivateAndroidComponent() 27 | } 28 | """ 29 | } 30 | val result = compilation.compile() 31 | 32 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 33 | 34 | val classLoader = result.classLoader 35 | val previewClass = classLoader.loadClass("test.PrivateAndroidPreviewKt") 36 | 37 | val previewFunction = previewClass.declaredMethods.find { it.name == "PrivateAndroidPreviewFunction" } 38 | assertThat(previewFunction).isNotNull 39 | assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() 40 | 41 | val privateComponent = previewClass.declaredMethods.find { it.name == "PrivateAndroidComponent" } 42 | assertThat(privateComponent).isNotNull 43 | assertThat(Modifier.isPrivate(privateComponent!!.modifiers)).isTrue() 44 | } 45 | 46 | @Test 47 | fun `ensure that internal androidx Preview functions are made public`() { 48 | val compilation = storytaleTest { 49 | "InternalAndroidPreview.kt" hasContent """ 50 | package test 51 | 52 | import androidx.compose.runtime.Composable 53 | 54 | @Composable 55 | internal fun InternalAndroidComponent() {} 56 | 57 | @androidx.compose.ui.tooling.preview.Preview 58 | @Composable 59 | internal fun InternalAndroidPreviewFunction() { 60 | InternalAndroidComponent() 61 | } 62 | """ 63 | } 64 | val result = compilation.compile() 65 | 66 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 67 | 68 | val classLoader = result.classLoader 69 | val previewClass = classLoader.loadClass("test.InternalAndroidPreviewKt") 70 | 71 | val previewFunction = previewClass.declaredMethods.find { it.name == "InternalAndroidPreviewFunction" } 72 | assertThat(previewFunction).isNotNull 73 | assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() 74 | 75 | val internalComponent = previewClass.declaredMethods.find { it.name == "InternalAndroidComponent" } 76 | assertThat(internalComponent).isNotNull 77 | assertThat(Modifier.isPublic(internalComponent!!.modifiers)).isTrue() 78 | } 79 | 80 | @Ignore("PreviewProcessor hasn't support protected Preview functions in classes yet") 81 | @Test 82 | fun `ensure that protected androidx Preview functions are made public in classes`() { 83 | val compilation = storytaleTest { 84 | "ProtectedAndroidPreview.kt" hasContent """ 85 | package test 86 | 87 | import androidx.compose.runtime.Composable 88 | 89 | open class PreviewAndroidContainer { 90 | @Composable 91 | protected fun ProtectedAndroidComponent() {} 92 | 93 | @androidx.compose.ui.tooling.preview.Preview 94 | @Composable 95 | protected fun ProtectedAndroidPreviewFunction() { 96 | ProtectedAndroidComponent() 97 | } 98 | } 99 | """ 100 | } 101 | val result = compilation.compile() 102 | 103 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 104 | 105 | val classLoader = result.classLoader 106 | val containerClass = classLoader.loadClass("test.PreviewAndroidContainer") 107 | 108 | val previewFunction = containerClass.declaredMethods.find { it.name == "ProtectedAndroidPreviewFunction" } 109 | assertThat(previewFunction).isNotNull 110 | assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() 111 | 112 | val protectedComponent = containerClass.declaredMethods.find { it.name == "ProtectedAndroidComponent" } 113 | assertThat(protectedComponent).isNotNull 114 | assertThat(Modifier.isPublic(protectedComponent!!.modifiers)).isTrue() 115 | } 116 | 117 | @Test 118 | fun `ensure that public androidx Preview functions remain public`() { 119 | val compilation = storytaleTest { 120 | "PublicAndroidPreview.kt" hasContent """ 121 | package test 122 | 123 | import androidx.compose.runtime.Composable 124 | 125 | @Composable 126 | fun PublicAndroidComponent() {} 127 | 128 | @androidx.compose.ui.tooling.preview.Preview 129 | @Composable 130 | fun PublicAndroidPreviewFunction() { 131 | PublicAndroidComponent() 132 | } 133 | """ 134 | } 135 | val result = compilation.compile() 136 | 137 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 138 | 139 | val classLoader = result.classLoader 140 | val previewClass = classLoader.loadClass("test.PublicAndroidPreviewKt") 141 | 142 | val previewFunction = previewClass.declaredMethods.find { it.name == "PublicAndroidPreviewFunction" } 143 | assertThat(previewFunction).isNotNull 144 | assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue() 145 | } 146 | } 147 | --------------------------------------------------------------------------------