Skip to content

Support Kotlin Multiplatform#983

Open
skydoves wants to merge 2 commits into
mainfrom
feature/kmp
Open

Support Kotlin Multiplatform#983
skydoves wants to merge 2 commits into
mainfrom
feature/kmp

Conversation

@skydoves

@skydoves skydoves commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Support Kotlin Multiplatform.

Summary by CodeRabbit

  • New Features

    • Added Kotlin Multiplatform support extending balloon to iOS, Desktop, and WebAssembly platforms.
    • Introduced new Compose-based API with BalloonState for visibility management and flexible positioning (TOP, BOTTOM, START, END, CENTER).
    • Added animation variants (FADE, ELASTIC, OVERSHOOT, CIRCULAR).
    • Included auto-dismiss functionality.
  • Documentation

    • Added migration guide for transitioning from legacy Android-only API to multiplatform approach.
  • Tests

    • Added position provider tests for positioning logic.

@skydoves skydoves self-assigned this Jun 14, 2026
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR introduces a new balloon-compose-multiplatform Kotlin Multiplatform library providing a Compose-based balloon popup system with shape geometry, state management, popup positioning with RTL/flip/clamp logic, and animated transitions. It adds a shared sample screen and platform entry points for Android, Desktop, iOS (Swift + Kotlin), and Wasm, with a full Gradle project wiring and migration documentation.

Changes

Balloon Compose Multiplatform Library

Layer / File(s) Summary
Project build infrastructure & module wiring
settings.gradle.kts, build.gradle.kts, gradle/libs.versions.toml, balloon-compose-multiplatform/build.gradle.kts, samples-shared/build.gradle.kts, androidApp/build.gradle.kts, desktopApp/build.gradle.kts, wasmApp/build.gradle.kts, .gitignore
Registers new submodules in settings, adds composeMultiplatform 1.10.3 and KMP/Compose plugin catalog entries, configures per-module build scripts with multi-target KMP setup and JVM target alignment, and enables KLib ABI validation in root apiValidation.
Public API contracts: enums, BalloonStyle, API dumps
balloon-compose-multiplatform/src/commonMain/.../ArrowOrientation.kt, ArrowPositionRules.kt, BalloonAlign.kt, BalloonAnimation.kt, BalloonCenterAlign.kt, BalloonStyle.kt, balloon-compose-multiplatform/api/...
Defines all public enum types (arrow direction/positioning/alignment/animation), the immutable BalloonStyle data class with defaults for geometry/colors/animation/dismissal, DefaultBalloonStyle, and generated API surface dumps for Android, Desktop, and KLib targets.
Shape geometry: BalloonShape and path builder
balloon-compose-multiplatform/src/commonMain/.../BalloonShape.kt, BalloonShapeBuilder.kt
Implements BalloonShape (a Compose Shape with rounded-rect body and triangular arrow notch) backed by buildBalloonPath and buildArrowTrianglePath geometry builders handling stroke insetting, corner/arrow clamping, position ratio, and RTL resolution.
BalloonState: visibility and alignment management
balloon-compose-multiplatform/src/commonMain/.../BalloonState.kt
Implements BalloonState using Compose mutableStateOf to track visibility, alignment, offset, and show generation; provides show/dismiss/toggle/showAtCenter methods and suspend await* helpers via snapshotFlow; adds rememberBalloonState factory.
Balloon builder, content rendering & transitions
balloon-compose-multiplatform/src/commonMain/.../Balloon.kt, BalloonTransitions.kt, TransformOrigins.kt
Adds the public Balloon.Builder (fluent API producing BalloonStyle), internal BalloonContent composable (clips/pads/draws balloon body with arrow overlay and border), @BalloonDsl/rememberBalloonBuilder, and enter/exit transition builders for all five BalloonAnimation modes.
Balloon composable and popup position provider
balloon-compose-multiplatform/src/commonMain/.../BalloonPopup.kt
Implements the public Balloon composable (anchor bounds capture, MutableTransitionState lifecycle, auto-dismiss coroutine, PopupProperties wiring, AnimatedVisibility) and internal BalloonPopupPositionProvider (placement for all alignments, flip logic, on-screen clamping, arrow ratio recalculation for ALIGN_ANCHOR, RTL resolution).
Position provider unit tests
balloon-compose-multiplatform/src/commonTest/.../PositionProviderTest.kt
Tests BalloonPopupPositionProvider.calculatePosition for all alignment modes, clamping, vertical flip, center alignment, both ArrowPositionRules modes, and RTL horizontal flip with assertions on resolvedArrowOrientation and resolvedArrowRatio.
Sample apps: BalloonDemoScreen and platform entry points
samples-shared/src/commonMain/.../BalloonDemoScreen.kt, samples-shared/src/iosMain/.../MainViewController.kt, androidApp/src/..., desktopApp/src/..., wasmApp/src/..., iosApp/...
Adds the shared BalloonDemoScreen composable demonstrating animations, styling, positioning, anchor API, and LazyColumn/nav demos; wires platform-specific entry points (Android KmpBalloonActivity, Desktop main, iOS Swift iOSApp/ContentView, Wasm main), iOS Xcode project, manifests, and asset catalogs.
Migration guide and library manifest
balloon-compose-multiplatform/MIGRATION.md, balloon-compose-multiplatform/src/androidMain/AndroidManifest.xml
Adds the migration guide translating Android Balloon.Builder setters to KMP equivalents, documents unsupported features, provides Old-vs-New API examples, and adds the minimal library AndroidManifest.xml.

Sequence Diagram

sequenceDiagram
  participant App as App (Android/Desktop/iOS/Wasm)
  participant BalloonComposable as Balloon composable
  participant BalloonState
  participant BalloonPopupPositionProvider
  participant BalloonContent

  App->>BalloonState: rememberBalloonState(style)
  App->>BalloonComposable: Balloon(state) { anchor content }
  BalloonComposable->>BalloonComposable: capture anchor bounds via onGloballyPositioned
  App->>BalloonState: show(BalloonAlign.BOTTOM)
  BalloonState->>BalloonState: isVisible=true, showGeneration++
  BalloonComposable->>BalloonPopupPositionProvider: calculatePosition(anchorBounds, windowBounds, popupSize)
  BalloonPopupPositionProvider->>BalloonPopupPositionProvider: compute placement, flip if needed, clamp on-screen
  BalloonPopupPositionProvider->>BalloonState: write resolvedArrowOrientation, resolvedArrowRatio
  BalloonPopupPositionProvider-->>BalloonComposable: IntOffset
  BalloonComposable->>BalloonContent: AnimatedVisibility(balloonEnterTransition) { BalloonContent(state, style) }
  BalloonContent->>BalloonContent: clip BalloonShape, draw arrow overlay, apply padding/border
  App->>BalloonState: dismiss()
  BalloonState->>BalloonState: isVisible=false, invoke onDismiss()
  BalloonComposable->>BalloonContent: AnimatedVisibility(balloonExitTransition) → unmount
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 A bunny carved paths through rounded-rect skies,
With arrows that point and transitions that rise!
On Android, Desktop, iOS, and Wasm it flies —
rememberBalloonState remembers, and never says bye.
RTL and clamping? All handled with care.
The balloon pops up — content floating on air! 🎈

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/kmp
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/kmp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (2)
androidApp/src/androidMain/AndroidManifest.xml (1)

19-23: ⚡ Quick win

Disable platform backups for the demo app.

android:allowBackup="true" enables app data backup/restore by default. For a demo app, hardening to false avoids accidental persistence/leakage of future local data.

Suggested manifest hardening
   <application
-    android:allowBackup="true"
+    android:allowBackup="false"
     android:label="Balloon KMP Demo"
     android:supportsRtl="true"
     android:theme="`@android`:style/Theme.Material.Light.NoActionBar">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@androidApp/src/androidMain/AndroidManifest.xml` around lines 19 - 23, The
android:allowBackup attribute in the application tag is currently set to true,
which enables automatic app data backup and restore functionality. For a demo
application, this should be disabled to prevent accidental persistence or
leakage of local data. Change the android:allowBackup attribute value from true
to false in the application element to harden the manifest configuration.
balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/Balloon.kt (1)

191-191: ⚡ Quick win

Move @BalloonDsl onto the receiver type.

Annotating rememberBalloonBuilder does not mark Balloon.Builder as a DSL receiver, so nested builder scopes stay unrestricted. Put the marker on Balloon.Builder (or on the function type) if you want the DSL guard to apply.

Suggested fix
-  public class Builder {
+  `@BalloonDsl`
+  public class Builder {
     private var cornerRadius: Dp = 5.dp
     ...
   }
 }
 
 /** DSL marker for the fluent [Balloon.Builder] receiver lambda. */
 `@DslMarker`
 public annotation class BalloonDsl
 
 `@Composable`
-@BalloonDsl
 public fun rememberBalloonBuilder(
   key: Any? = null,
   block: Balloon.Builder.() -> Unit,
 ): BalloonStyle = remember(key) { Balloon.Builder().apply(block).build() }

Also applies to: 382-387

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/Balloon.kt`
at line 191, The `@BalloonDsl` annotation needs to be moved from the
rememberBalloonBuilder function to the actual receiver type to properly enforce
DSL scope restrictions. At line 191, add the `@BalloonDsl` annotation directly to
the public class Builder declaration for Balloon.Builder. Apply the same fix to
the other affected Builder class at lines 382-387 by adding `@BalloonDsl` to its
class declaration as well. This ensures the DSL guard applies to nested builder
scopes in both locations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@balloon-compose-multiplatform/MIGRATION.md`:
- Around line 150-151: The migration documentation for
setOnBalloonDismissListener is currently directing users to observe
BalloonState.isVisible or use LaunchedEffect, which is imprecise and misses the
intended transition semantics. Update the migration table entry for
setOnBalloonDismissListener to instead map it directly to
BalloonState.onDismiss, since that callback already provides the same
visible→hidden semantics as a one-shot handler and is the more direct and
accurate migration path.
- Around line 139-147: The Markdown table contains raw pipe characters (`|`)
within cell content that are breaking the table structure. Escape these pipes by
replacing them with `\|`. Specifically, in entries like
`setTextTypeface(Int|Typeface)` and `setLayout(View|`@LayoutRes`
Int|ViewBinding)`, add backslashes before each pipe character that appears
within the type signatures and method names to properly escape them in the
Markdown table format.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/Balloon.kt`:
- Around line 315-318: The setAnimationDurationMillis method accepts animation
duration values without validation, but these values are passed to tween(...)
which requires non-negative durations per its `@IntRange`(from = 0) constraint.
Clamp the input value to zero if it's negative before assigning it to
animationDurationMillis, using the same validation pattern already implemented
in setAutoDismissDuration to prevent runtime failures when negative durations
are provided.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonPopup.kt`:
- Around line 250-256: The arrow orientation mapping for side-aligned balloons
(START and END) incorrectly applies RTL logic that causes the arrow to render on
the wrong side initially. Replace the RTL conditional logic in the
BalloonAlign.START and BalloonAlign.END branches with their logical opposite
edges: BalloonAlign.START should always map to ArrowOrientation.END, and
BalloonAlign.END should always map to ArrowOrientation.START, regardless of the
isRtl value. This ensures the arrow is positioned on the correct logical
opposite edge from the start, preventing incorrect animation pivot and rendering
in the first frame.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonShapeBuilder.kt`:
- Around line 194-261: The arrow overlay triangle is being positioned using
different clamping logic than the balloon body notch in buildBalloonPath,
causing misalignment when the arrow has a different color than the background
near rounded corners. In buildArrowTrianglePath, the center position
calculations (centerX for TOP and BOTTOM cases, centerY for LEFT and RIGHT
cases) are currently clamping only against halfArrow + halfStrokePx bounds. You
need to identify how buildBalloonPath clamps the center position (which includes
radius + halfArrow constraints) and apply that same clamping logic to all four
when branches (TOP, BOTTOM, LEFT, RIGHT) in buildArrowTrianglePath so the arrow
overlay aligns with the notch.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonState.kt`:
- Around line 164-174: The show() function in BalloonState needs to reset the
resolved arrow cache properties when displaying a new balloon. Currently,
resolvedArrowOrientation and resolvedArrowRatio persist across dismiss/show
cycles, causing stale arrow placement data to be used in the first frame after
reopening. Add lines in the show() function to reset resolvedArrowOrientation
and resolvedArrowRatio to their default/initial values, similar to how
centerAlign is reset to null, to ensure fresh arrow placement computation on
each show cycle. Also check lines 200-210 for any related show method or
overload that requires the same fix.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonStyle.kt`:
- Around line 66-84: The BalloonStyle data class constructor does not validate
its documented invariants, allowing invalid values to propagate downstream. Add
an init block to the BalloonStyle data class that validates: arrowPosition must
be in the range 0f to 1f (inclusive), animationDurationMillis must be
non-negative, and autoDismissMillis must be non-negative. Each validation should
throw an IllegalArgumentException with a descriptive message if the constraint
is violated, ensuring that only valid BalloonStyle instances can be constructed.

In `@desktopApp/build.gradle.kts`:
- Around line 48-49: The packageVersion property in desktopApp/build.gradle.kts
is hardcoded to "1.0.0", which can cause version drift from the project's actual
release version. Replace the hardcoded packageVersion value with a reference to
the project's shared version source (such as the project.version property or a
version variable defined in the gradle configuration). This ensures the desktop
distribution version stays synchronized with the project's canonical release
version definition.

In `@iosApp/iosApp.xcodeproj/project.pbxproj`:
- Line 334: The PRODUCT_BUNDLE_IDENTIFIER is incorrectly composed by
concatenating BUNDLE_ID with TEAM_ID, which violates iOS provisioning
conventions and can break signing. In iosApp/iosApp.xcodeproj/project.pbxproj at
line 334 and line 361, change PRODUCT_BUNDLE_IDENTIFIER from
"${BUNDLE_ID}${TEAM_ID}" to just "${BUNDLE_ID}" (removing the TEAM_ID
concatenation). The TEAM_ID should only be used in the DEVELOPMENT_TEAM setting,
not in the bundle identifier itself.

In
`@iosApp/iosApp.xcodeproj/xcuserdata/jaewoong.xcuserdatad/xcschemes/iosApp.xcscheme`:
- Around line 1-32: The iosApp.xcscheme file is located under xcuserdata, which
contains user-specific Xcode configuration and should not be committed to
version control. Remove this file from the pull request by excluding the entire
xcuserdata directory from git tracking, and ensure xcuserdata is listed in the
.gitignore file. If a shared Xcode scheme is needed across team members, move
the scheme file to xcshareddata/xcschemes instead, which is the appropriate
location for shared build configurations.

In `@iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json`:
- Around line 2-12: The AppIcon.appiconset Contents.json file defines image
entries without corresponding filename references. For the universal platform
iOS image entry at 1024x1024 size, add a "filename" key that points to the
actual PNG image file (e.g., "AppIcon-1024.png"), then ensure the actual image
file with that name is present in the AppIcon.appiconset directory. If multiple
icon sizes are needed for iOS, add additional image entries to the Contents.json
array with appropriate sizes, idioms, and filenames, along with their
corresponding image files in the directory, so that Xcode can properly bundle
the app icon during the build process.

In `@wasmApp/src/wasmJsMain/resources/index.html`:
- Line 8: The script tag loading composeApp.js in the index.html file does not
have the defer attribute, causing it to execute before the body element is
available. This causes a crash in Main.kt when attempting to access
document.body. Add the defer attribute to the script tag that loads
composeApp.js to defer its execution until after the DOM is fully loaded and the
body element is available.

---

Nitpick comments:
In `@androidApp/src/androidMain/AndroidManifest.xml`:
- Around line 19-23: The android:allowBackup attribute in the application tag is
currently set to true, which enables automatic app data backup and restore
functionality. For a demo application, this should be disabled to prevent
accidental persistence or leakage of local data. Change the android:allowBackup
attribute value from true to false in the application element to harden the
manifest configuration.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/Balloon.kt`:
- Line 191: The `@BalloonDsl` annotation needs to be moved from the
rememberBalloonBuilder function to the actual receiver type to properly enforce
DSL scope restrictions. At line 191, add the `@BalloonDsl` annotation directly to
the public class Builder declaration for Balloon.Builder. Apply the same fix to
the other affected Builder class at lines 382-387 by adding `@BalloonDsl` to its
class declaration as well. This ensures the DSL guard applies to nested builder
scopes in both locations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3af4c74-5676-44b8-a8ae-de5f716bd32a

📥 Commits

Reviewing files that changed from the base of the PR and between d96951f and 7ac57f4.

📒 Files selected for processing (47)
  • .gitignore
  • androidApp/build.gradle.kts
  • androidApp/src/androidMain/AndroidManifest.xml
  • androidApp/src/androidMain/kotlin/com/skydoves/balloon/kmpdemo/KmpBalloonActivity.kt
  • balloon-compose-multiplatform/MIGRATION.md
  • balloon-compose-multiplatform/api/android/balloon-compose-multiplatform.api
  • balloon-compose-multiplatform/api/balloon-compose-multiplatform.klib.api
  • balloon-compose-multiplatform/api/desktop/balloon-compose-multiplatform.api
  • balloon-compose-multiplatform/build.gradle.kts
  • balloon-compose-multiplatform/src/androidMain/AndroidManifest.xml
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/ArrowOrientation.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/ArrowPositionRules.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/Balloon.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonAlign.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonAnimation.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonCenterAlign.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonPopup.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonShape.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonShapeBuilder.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonState.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonStyle.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonTransitions.kt
  • balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/TransformOrigins.kt
  • balloon-compose-multiplatform/src/commonTest/kotlin/com/skydoves/balloon/compose/multiplatform/PositionProviderTest.kt
  • build.gradle.kts
  • desktopApp/build.gradle.kts
  • desktopApp/src/desktopMain/kotlin/Main.kt
  • gradle/libs.versions.toml
  • iosApp/Configuration/Config.xcconfig
  • iosApp/iosApp.xcodeproj/project.pbxproj
  • iosApp/iosApp.xcodeproj/xcuserdata/jaewoong.xcuserdatad/xcschemes/iosApp.xcscheme
  • iosApp/iosApp.xcodeproj/xcuserdata/jaewoong.xcuserdatad/xcschemes/xcschememanagement.plist
  • iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
  • iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
  • iosApp/iosApp/Assets.xcassets/Contents.json
  • iosApp/iosApp/ContentView.swift
  • iosApp/iosApp/Info.plist
  • iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
  • iosApp/iosApp/iOSApp.swift
  • samples-shared/build.gradle.kts
  • samples-shared/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/sample/BalloonDemoScreen.kt
  • samples-shared/src/iosMain/kotlin/com/skydoves/balloon/compose/multiplatform/sample/MainViewController.kt
  • settings.gradle.kts
  • wasmApp/build.gradle.kts
  • wasmApp/src/wasmJsMain/kotlin/Main.kt
  • wasmApp/src/wasmJsMain/resources/index.html
  • wasmApp/src/wasmJsMain/resources/style.css

Comment on lines +139 to +147
| `setTextTypeface(Int|Typeface)` | _not supported in KMP_ | `Typeface` is Android-only; pass a `FontFamily` to the `Text(...)` directly. |
| `setTextLineSpacing(...)` / `setTextLetterSpacing(...)` (+ `*Resource`) | _not supported in KMP_ | Set on the `Text(...)` directly. |
| `setIncludeFontPadding(...)` | _not supported in KMP_ | Android-`TextView`-specific. |
| `setTextGravity(Int)` | _not supported in KMP_ | Use `Text(textAlign = ...)`. |
| `setTextForm(TextForm)` | _not supported in KMP_ | View-only abstraction. |
| `setIcon*` (all variants) | _not a builder concern_ | Compose icons go inside the slot — `Row { Icon(...); Text(...) }`. |
| `setAlpha(Float)` | _not supported in KMP_ | Wrap the slot in `Modifier.alpha(...)` if needed. |
| `setElevation(Int)` (+ `*Resource`) | _not supported in KMP_ | Wrap the slot in `Modifier.shadow(...)`. |
| `setLayout(View|@LayoutRes Int|ViewBinding)` | _not a builder concern_ | The whole point of the Compose slot. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape the | characters in these table entries.

Raw pipes inside the type names are breaking the Markdown table structure, which is why markdownlint flags these rows and the rendered docs can lose part of the signature.

Fix
-| `setTextTypeface(Int|Typeface)` | _not supported in KMP_ | `Typeface` is Android-only; pass a `FontFamily` to the `Text(...)` directly. |
+| `setTextTypeface(Int\|Typeface)` | _not supported in KMP_ | `Typeface` is Android-only; pass a `FontFamily` to the `Text(...)` directly. |
...
-| `setLayout(View|`@LayoutRes` Int|ViewBinding)` | _not a builder concern_ | The whole point of the Compose slot. |
+| `setLayout(View\|`@LayoutRes` Int\|ViewBinding)` | _not a builder concern_ | The whole point of the Compose slot. |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 139-139: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 147-147: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@balloon-compose-multiplatform/MIGRATION.md` around lines 139 - 147, The
Markdown table contains raw pipe characters (`|`) within cell content that are
breaking the table structure. Escape these pipes by replacing them with `\|`.
Specifically, in entries like `setTextTypeface(Int|Typeface)` and
`setLayout(View|`@LayoutRes` Int|ViewBinding)`, add backslashes before each pipe
character that appears within the type signatures and method names to properly
escape them in the Markdown table format.

Source: Linters/SAST tools

Comment on lines +150 to +151
| `setOnBalloonDismissListener(...)` | Observe `BalloonState.isVisible` | Or compose a `LaunchedEffect` keyed on the state. |
| `setOnBalloonInitializedListener(...)` | _not needed in KMP_ | Compose callbacks (`onGloballyPositioned`) fire naturally. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Map the legacy dismiss listener to BalloonState.onDismiss.

BalloonState already exposes a one-shot onDismiss callback with the same visible→hidden semantics, so telling migrators to observe isVisible or wire a LaunchedEffect is less precise and can miss the intended transition.

Fix
-| `setOnBalloonDismissListener(...)` | Observe `BalloonState.isVisible` | Or compose a `LaunchedEffect` keyed on the state. |
+| `setOnBalloonDismissListener(...)` | `state.onDismiss = { ... }` | Direct KMP equivalent; fires once per visible→hidden transition. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `setOnBalloonDismissListener(...)` | Observe `BalloonState.isVisible` | Or compose a `LaunchedEffect` keyed on the state. |
| `setOnBalloonInitializedListener(...)` | _not needed in KMP_ | Compose callbacks (`onGloballyPositioned`) fire naturally. |
| `setOnBalloonDismissListener(...)` | `state.onDismiss = { ... }` | Direct KMP equivalent; fires once per visible→hidden transition. |
| `setOnBalloonInitializedListener(...)` | _not needed in KMP_ | Compose callbacks (`onGloballyPositioned`) fire naturally. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@balloon-compose-multiplatform/MIGRATION.md` around lines 150 - 151, The
migration documentation for setOnBalloonDismissListener is currently directing
users to observe BalloonState.isVisible or use LaunchedEffect, which is
imprecise and misses the intended transition semantics. Update the migration
table entry for setOnBalloonDismissListener to instead map it directly to
BalloonState.onDismiss, since that callback already provides the same
visible→hidden semantics as a one-shot handler and is the more direct and
accurate migration path.

Comment on lines +315 to +318
/** Sets the duration of the [BalloonAnimation] in milliseconds. */
public fun setAnimationDurationMillis(value: Int): Builder = apply {
animationDurationMillis = value
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the Balloon.kt file and examine the setAnimationDurationMillis method
find . -name "Balloon.kt" -type f | head -20

Repository: skydoves/Balloon

Length of output: 300


🏁 Script executed:

# Find and examine BalloonTransitions.kt to see how animationDurationMillis is used
find . -name "*Transitions*" -o -name "*transitions*" | grep -i kotlin

Repository: skydoves/Balloon

Length of output: 179


🏁 Script executed:

# Search for setAutoDismissDuration to compare validation approach
rg "setAutoDismissDuration" -A 5 -B 2

Repository: skydoves/Balloon

Length of output: 17517


🏁 Script executed:

# Read the target file to see setAnimationDurationMillis and the animationDurationMillis field
cat -n balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/Balloon.kt | sed -n '1,350p'

Repository: skydoves/Balloon

Length of output: 16898


🏁 Script executed:

# Read BalloonTransitions.kt to see how animationDurationMillis is used
cat -n balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonTransitions.kt

Repository: skydoves/Balloon

Length of output: 5651


🏁 Script executed:

# Check where animationDurationMillis from the builder is used
rg "animationDurationMillis" balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/ -A 3 -B 3

Repository: skydoves/Balloon

Length of output: 7361


🏁 Script executed:

# Check the rest of the Balloon.kt file to see the build() method
cat -n balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/Balloon.kt | sed -n '340,380p'

Repository: skydoves/Balloon

Length of output: 1814


🌐 Web query:

Compose animation tween negative duration behavior runtime error

💡 Result:

In Jetpack Compose, animation specifications that rely on duration—such as tween, keyframes, and repeatable—require a non-negative duration [1][2][3]. The durationMillis property in TweenSpec and related DurationBasedAnimationSpec implementations is explicitly annotated with @IntRange(from = 0L) [2][3]. Providing a negative value to these constructors will violate this range constraint. Depending on the environment (e.g., if validation is enabled via annotations or during internal framework processing), this may result in an IllegalArgumentException or undefined animation behavior, such as the animation failing to trigger or completing instantly [2][3]. To ensure stable behavior, always provide a duration of 0 or greater [2][3]. If you do not require a specific duration, using the default DefaultDurationMillis (which is 300ms) or a positive integer is the standard practice [2][3][4]. If an animation must appear to happen instantly, snap() is the recommended alternative rather than using a zero or negative duration [5].

Citations:


Validate animation durations to prevent runtime failures.

setAnimationDurationMillis stores raw values without validation, but they're passed directly to tween(...) in balloonEnterTransition / balloonExitTransition. Negative durations violate Compose's @IntRange(from = 0) constraint on animation specs and will fail at runtime. Clamp values to zero, matching the validation pattern already used in setAutoDismissDuration.

Suggested fix
 public fun setAnimationDurationMillis(value: Int): Builder = apply {
-  animationDurationMillis = value
+  animationDurationMillis = value.coerceAtLeast(0)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Sets the duration of the [BalloonAnimation] in milliseconds. */
public fun setAnimationDurationMillis(value: Int): Builder = apply {
animationDurationMillis = value
}
/** Sets the duration of the [BalloonAnimation] in milliseconds. */
public fun setAnimationDurationMillis(value: Int): Builder = apply {
animationDurationMillis = value.coerceAtLeast(0)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/Balloon.kt`
around lines 315 - 318, The setAnimationDurationMillis method accepts animation
duration values without validation, but these values are passed to tween(...)
which requires non-negative durations per its `@IntRange`(from = 0) constraint.
Clamp the input value to zero if it's negative before assigning it to
animationDurationMillis, using the same validation pattern already implemented
in setAutoDismissDuration to prevent runtime failures when negative durations
are provided.

Comment on lines +250 to +256
return when (align) {
BalloonAlign.TOP -> ArrowOrientation.BOTTOM
BalloonAlign.BOTTOM -> ArrowOrientation.TOP
// Balloon on the leading side -> arrow points back to the trailing side.
BalloonAlign.START -> if (isRtl) ArrowOrientation.START else ArrowOrientation.END
// Balloon on the trailing side -> arrow points back to the leading side.
BalloonAlign.END -> if (isRtl) ArrowOrientation.END else ArrowOrientation.START

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the logical opposite edge in the fallback START/END mapping.

For side-aligned balloons, the arrow should sit on the logical opposite edge regardless of layout direction: START -> END, END -> START. The current RTL branches invert that, so the first frame in RTL can draw the arrow and animation pivot on the wrong side before the position provider corrects it.

Suggested fix
   return when (align) {
     BalloonAlign.TOP -> ArrowOrientation.BOTTOM
     BalloonAlign.BOTTOM -> ArrowOrientation.TOP
-    BalloonAlign.START -> if (isRtl) ArrowOrientation.START else ArrowOrientation.END
-    BalloonAlign.END -> if (isRtl) ArrowOrientation.END else ArrowOrientation.START
+    BalloonAlign.START -> ArrowOrientation.END
+    BalloonAlign.END -> ArrowOrientation.START
     BalloonAlign.CENTER -> ArrowOrientation.BOTTOM
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return when (align) {
BalloonAlign.TOP -> ArrowOrientation.BOTTOM
BalloonAlign.BOTTOM -> ArrowOrientation.TOP
// Balloon on the leading side -> arrow points back to the trailing side.
BalloonAlign.START -> if (isRtl) ArrowOrientation.START else ArrowOrientation.END
// Balloon on the trailing side -> arrow points back to the leading side.
BalloonAlign.END -> if (isRtl) ArrowOrientation.END else ArrowOrientation.START
return when (align) {
BalloonAlign.TOP -> ArrowOrientation.BOTTOM
BalloonAlign.BOTTOM -> ArrowOrientation.TOP
// Balloon on the leading side -> arrow points back to the trailing side.
BalloonAlign.START -> ArrowOrientation.END
// Balloon on the trailing side -> arrow points back to the leading side.
BalloonAlign.END -> ArrowOrientation.START
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonPopup.kt`
around lines 250 - 256, The arrow orientation mapping for side-aligned balloons
(START and END) incorrectly applies RTL logic that causes the arrow to render on
the wrong side initially. Replace the RTL conditional logic in the
BalloonAlign.START and BalloonAlign.END branches with their logical opposite
edges: BalloonAlign.START should always map to ArrowOrientation.END, and
BalloonAlign.END should always map to ArrowOrientation.START, regardless of the
isRtl value. This ensures the arrow is positioned on the correct logical
opposite edge from the start, preventing incorrect animation pivot and rendering
in the first frame.

Comment on lines +194 to +261
internal fun buildArrowTrianglePath(
size: Size,
arrowWidthPx: Float,
arrowHeightPx: Float,
side: ResolvedArrowSide,
ratioInRect: Float,
halfStrokePx: Float,
): Path {
val path = Path()
if (arrowWidthPx <= 0f || arrowHeightPx <= 0f) return path
if (size.width <= 0f || size.height <= 0f) return path

val protrusion = arrowHeightPx * 0.5f
val extra = protrusion + halfStrokePx
val rectLeft = if (side == ResolvedArrowSide.LEFT) extra else halfStrokePx
val rectTop = if (side == ResolvedArrowSide.TOP) extra else halfStrokePx
val rectRight = if (side == ResolvedArrowSide.RIGHT) {
size.width - extra
} else {
size.width - halfStrokePx
}
val rectBottom = if (side == ResolvedArrowSide.BOTTOM) {
size.height - extra
} else {
size.height - halfStrokePx
}

val halfArrow = arrowWidthPx / 2f

when (side) {
ResolvedArrowSide.TOP -> {
val centerX = (size.width * ratioInRect)
.coerceIn(halfArrow + halfStrokePx, size.width - halfArrow - halfStrokePx)
val tipY = rectTop - protrusion
path.moveTo(centerX - halfArrow, rectTop)
path.lineTo(centerX, tipY)
path.lineTo(centerX + halfArrow, rectTop)
path.close()
}
ResolvedArrowSide.BOTTOM -> {
val centerX = (size.width * ratioInRect)
.coerceIn(halfArrow + halfStrokePx, size.width - halfArrow - halfStrokePx)
val tipY = rectBottom + protrusion
path.moveTo(centerX - halfArrow, rectBottom)
path.lineTo(centerX, tipY)
path.lineTo(centerX + halfArrow, rectBottom)
path.close()
}
ResolvedArrowSide.LEFT -> {
val centerY = (size.height * ratioInRect)
.coerceIn(halfArrow + halfStrokePx, size.height - halfArrow - halfStrokePx)
val tipX = rectLeft - protrusion
path.moveTo(rectLeft, centerY - halfArrow)
path.lineTo(tipX, centerY)
path.lineTo(rectLeft, centerY + halfArrow)
path.close()
}
ResolvedArrowSide.RIGHT -> {
val centerY = (size.height * ratioInRect)
.coerceIn(halfArrow + halfStrokePx, size.height - halfArrow - halfStrokePx)
val tipX = rectRight + protrusion
path.moveTo(rectRight, centerY - halfArrow)
path.lineTo(tipX, centerY)
path.lineTo(rectRight, centerY + halfArrow)
path.close()
}
}
return path

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the colored arrow overlay on the same clamped center as the body notch.

buildArrowTrianglePath only clamps against the stroke bounds, while buildBalloonPath also clamps against radius + halfArrow. When arrowColor != backgroundColor and the arrow is near a rounded corner, the overlay triangle no longer sits in the notch and leaves a visible seam. Reuse the same center calculation here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@balloon-compose-multiplatform/src/commonMain/kotlin/com/skydoves/balloon/compose/multiplatform/BalloonShapeBuilder.kt`
around lines 194 - 261, The arrow overlay triangle is being positioned using
different clamping logic than the balloon body notch in buildBalloonPath,
causing misalignment when the arrow has a different color than the background
near rounded corners. In buildArrowTrianglePath, the center position
calculations (centerX for TOP and BOTTOM cases, centerY for LEFT and RIGHT
cases) are currently clamping only against halfArrow + halfStrokePx bounds. You
need to identify how buildBalloonPath clamps the center position (which includes
radius + halfArrow constraints) and apply that same clamping logic to all four
when branches (TOP, BOTTOM, LEFT, RIGHT) in buildArrowTrianglePath so the arrow
overlay aligns with the notch.

Comment on lines +48 to +49
packageName = "BalloonComposeMultiplatformDemo"
packageVersion = "1.0.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align desktop distribution version with shared release version source.

Line 49 hardcodes packageVersion = "1.0.0", which can drift from the project’s actual release versioning and produce mismatched desktop artifacts.

Suggested fix
+import com.skydoves.balloon.Configuration
 import org.jetbrains.compose.desktop.application.dsl.TargetFormat
@@
-      packageVersion = "1.0.0"
+      packageVersion = Configuration.versionName
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
packageName = "BalloonComposeMultiplatformDemo"
packageVersion = "1.0.0"
import com.skydoves.balloon.Configuration
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
packageName = "BalloonComposeMultiplatformDemo"
packageVersion = Configuration.versionName
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktopApp/build.gradle.kts` around lines 48 - 49, The packageVersion
property in desktopApp/build.gradle.kts is hardcoded to "1.0.0", which can cause
version drift from the project's actual release version. Replace the hardcoded
packageVersion value with a reference to the project's shared version source
(such as the project.version property or a version variable defined in the
gradle configuration). This ensures the desktop distribution version stays
synchronized with the project's canonical release version definition.

"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a stable bundle identifier; do not append team ID.

At Line 334 and Line 361, PRODUCT_BUNDLE_IDENTIFIER is composed as "${BUNDLE_ID}${TEAM_ID}". TEAM_ID belongs in DEVELOPMENT_TEAM, not in the bundle ID. This can produce unintended identifiers and break provisioning/signing expectations.

Suggested fix
-				PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
+				PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}";

Also applies to: 361-361

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@iosApp/iosApp.xcodeproj/project.pbxproj` at line 334, The
PRODUCT_BUNDLE_IDENTIFIER is incorrectly composed by concatenating BUNDLE_ID
with TEAM_ID, which violates iOS provisioning conventions and can break signing.
In iosApp/iosApp.xcodeproj/project.pbxproj at line 334 and line 361, change
PRODUCT_BUNDLE_IDENTIFIER from "${BUNDLE_ID}${TEAM_ID}" to just "${BUNDLE_ID}"
(removing the TEAM_ID concatenation). The TEAM_ID should only be used in the
DEVELOPMENT_TEAM setting, not in the bundle identifier itself.

Comment on lines +1 to +32
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
version = "1.3">
<BuildAction>
<BuildActionEntries>
<BuildActionEntry
buildForRunning = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7555FF7A242A565900829871"
BuildableName = "BalloonCMPDemo.app"
BlueprintName = "iosApp"
ReferencedContainer = "container:iosApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<LaunchAction
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
allowLocationSimulation = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7555FF7A242A565900829871"
BuildableName = "BalloonCMPDemo.app"
BlueprintName = "iosApp"
ReferencedContainer = "container:iosApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
</Scheme>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Do not commit user-specific Xcode scheme data.

This file is under xcuserdata, which is local-user state and should stay untracked. Keep shared schemes under xcshareddata/xcschemes if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@iosApp/iosApp.xcodeproj/xcuserdata/jaewoong.xcuserdatad/xcschemes/iosApp.xcscheme`
around lines 1 - 32, The iosApp.xcscheme file is located under xcuserdata, which
contains user-specific Xcode configuration and should not be committed to
version control. Remove this file from the pull request by excluding the entire
xcuserdata directory from git tracking, and ensure xcuserdata is listed in the
.gitignore file. If a shared Xcode scheme is needed across team members, move
the scheme file to xcshareddata/xcschemes instead, which is the appropriate
location for shared build configurations.

Comment on lines +2 to +12
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -a . iosApp/iosApp/Assets.xcassets/AppIcon.appiconset
rg -n '"filename"\s*:' iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json

Repository: skydoves/Balloon

Length of output: 142


Add image file(s) to the AppIcon asset set.

The AppIcon.appiconset contains only a metadata file with no filename entry and no accompanying image files. Xcode cannot bundle an app icon without at least one valid image reference. Either add the icon PNG file(s) to this directory and reference them in Contents.json with a "filename" key, or verify the icon is being sourced from elsewhere in the build process.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json` around lines
2 - 12, The AppIcon.appiconset Contents.json file defines image entries without
corresponding filename references. For the universal platform iOS image entry at
1024x1024 size, add a "filename" key that points to the actual PNG image file
(e.g., "AppIcon-1024.png"), then ensure the actual image file with that name is
present in the AppIcon.appiconset directory. If multiple icon sizes are needed
for iOS, add additional image entries to the Contents.json array with
appropriate sizes, idioms, and filenames, along with their corresponding image
files in the directory, so that Xcode can properly bundle the app icon during
the build process.

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Balloon Compose Multiplatform - Wasm Demo</title>
<link type="text/css" rel="stylesheet" href="style.css">
<script type="application/javascript" src="composeApp.js"></script>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Defer Wasm bundle loading to avoid early startup crash.

At Line 8, loading composeApp.js in <head> without defer can run before <body> is available, which can crash wasmApp/src/wasmJsMain/kotlin/Main.kt at Line 24 (document.body!!).

Suggested fix
-  <script type="application/javascript" src="composeApp.js"></script>
+  <script type="application/javascript" src="composeApp.js" defer></script>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wasmApp/src/wasmJsMain/resources/index.html` at line 8, The script tag
loading composeApp.js in the index.html file does not have the defer attribute,
causing it to execute before the body element is available. This causes a crash
in Main.kt when attempting to access document.body. Add the defer attribute to
the script tag that loads composeApp.js to defer its execution until after the
DOM is fully loaded and the body element is available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant