diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d33f43..6348b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [1.0.0-beta16] - 2026-08-18 + +### Features +- New `ConnectorStyle.TEARDROP`: a rounded teardrop nub anchored directly on the tooltip card edge and pointing toward the target — no connector line at all, like a classic speech-bubble tail +- New `ConnectorEndStyle.TEARDROP`: a rounded petal-shaped connector line endpoint, as a smoother alternative to `ARROW` +- `CoachmarkConfig.connectorTeardropLength` / `connectorTeardropWidth` to size the teardrop shape +- `CoachmarkConfig.connectorTeardropTargetGap` to control the gap left between the teardrop nub and the target it points at +- `CoachmarkColors.connectorTeardropColor` to color the teardrop shape independently of `connectorColor` — e.g. match it to the tooltip card background for a seamless attached-tail look + +### Sample App +- Added "Teardrop Connector" and "Teardrop Endpoint" demos to the Connector Styles example + ## [1.0.0-beta15] - 2026-03-18 ### Features diff --git a/lumen/api/android/lumen.api b/lumen/api/android/lumen.api index 69e1a26..dbd0370 100644 --- a/lumen/api/android/lumen.api +++ b/lumen/api/android/lumen.api @@ -32,11 +32,12 @@ public final class io/luminos/CoachmarkAnalytics { public final class io/luminos/CoachmarkColors { public static final field $stable I - public synthetic fun (JJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (JJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-0d7_KjU ()J public final fun component10-0d7_KjU ()J public final fun component11-0d7_KjU ()J + public final fun component12-0d7_KjU ()J public final fun component2-0d7_KjU ()J public final fun component3-0d7_KjU ()J public final fun component4-0d7_KjU ()J @@ -45,10 +46,11 @@ public final class io/luminos/CoachmarkColors { public final fun component7-0d7_KjU ()J public final fun component8-0d7_KjU ()J public final fun component9-0d7_KjU ()J - public final fun copy-KpyCeHw (JJJJJJJJJJJ)Lio/luminos/CoachmarkColors; - public static synthetic fun copy-KpyCeHw$default (Lio/luminos/CoachmarkColors;JJJJJJJJJJJILjava/lang/Object;)Lio/luminos/CoachmarkColors; + public final fun copy-2qZNXz8 (JJJJJJJJJJJJ)Lio/luminos/CoachmarkColors; + public static synthetic fun copy-2qZNXz8$default (Lio/luminos/CoachmarkColors;JJJJJJJJJJJJILjava/lang/Object;)Lio/luminos/CoachmarkColors; public fun equals (Ljava/lang/Object;)Z public final fun getConnectorColor-0d7_KjU ()J + public final fun getConnectorTeardropColor-0d7_KjU ()J public final fun getCtaButtonColor-0d7_KjU ()J public final fun getCtaTextColor-0d7_KjU ()J public final fun getDescriptionColor-0d7_KjU ()J @@ -72,48 +74,51 @@ public final class io/luminos/CoachmarkColorsKt { public final class io/luminos/CoachmarkConfig { public static final field $stable I - public synthetic fun (FFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (FFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-D9Ej5fM ()F - public final fun component10 ()I - public final fun component11 ()I - public final fun component12 ()I - public final fun component13 ()Lio/luminos/ScrimOpacity; - public final fun component14 ()Lio/luminos/ScrimTapBehavior; - public final fun component15 ()Z - public final fun component16 ()Z - public final fun component17 ()Lio/luminos/BackPressBehavior; - public final fun component18 ()Lio/luminos/HighlightAnimation; - public final fun component19 ()I + public final fun component10-D9Ej5fM ()F + public final fun component11-D9Ej5fM ()F + public final fun component12-D9Ej5fM ()F + public final fun component13 ()I + public final fun component14 ()I + public final fun component15 ()I + public final fun component16 ()Lio/luminos/ScrimOpacity; + public final fun component17 ()Lio/luminos/ScrimTapBehavior; + public final fun component18 ()Z + public final fun component19 ()Z public final fun component2-D9Ej5fM ()F - public final fun component20 ()Z - public final fun component21 ()Ljava/lang/String; - public final fun component22 ()J + public final fun component20 ()Lio/luminos/BackPressBehavior; + public final fun component21 ()Lio/luminos/HighlightAnimation; + public final fun component22 ()I public final fun component23 ()Z - public final fun component24 ()J - public final fun component25 ()Ljava/lang/String; - public final fun component26 ()J - public final fun component27 ()Z - public final fun component28-D9Ej5fM ()F - public final fun component29-D9Ej5fM ()F + public final fun component24 ()Ljava/lang/String; + public final fun component25 ()J + public final fun component26 ()Z + public final fun component27 ()J + public final fun component28 ()Ljava/lang/String; + public final fun component29 ()J public final fun component3-D9Ej5fM ()F - public final fun component30-D9Ej5fM ()F + public final fun component30 ()Z public final fun component31-D9Ej5fM ()F - public final fun component32-e0LSkKk ()I - public final fun component33-e0LSkKk ()I - public final fun component34-e0LSkKk ()I + public final fun component32-D9Ej5fM ()F + public final fun component33-D9Ej5fM ()F + public final fun component34-D9Ej5fM ()F public final fun component35-e0LSkKk ()I public final fun component36-e0LSkKk ()I - public final fun component37 ()Landroidx/compose/foundation/layout/Arrangement$Horizontal; - public final fun component38 ()Z + public final fun component37-e0LSkKk ()I + public final fun component38-e0LSkKk ()I + public final fun component39-e0LSkKk ()I public final fun component4 ()F - public final fun component5 ()Lkotlin/jvm/functions/Function3; + public final fun component40 ()Landroidx/compose/foundation/layout/Arrangement$Horizontal; + public final fun component41 ()Z + public final fun component5-D9Ej5fM ()F public final fun component6-D9Ej5fM ()F public final fun component7-D9Ej5fM ()F - public final fun component8-D9Ej5fM ()F + public final fun component8 ()Lkotlin/jvm/functions/Function3; public final fun component9-D9Ej5fM ()F - public final fun copy-ZvfVXdg (FFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;Z)Lio/luminos/CoachmarkConfig; - public static synthetic fun copy-ZvfVXdg$default (Lio/luminos/CoachmarkConfig;FFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZIILjava/lang/Object;)Lio/luminos/CoachmarkConfig; + public final fun copy-I2SBm6M (FFFFFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;Z)Lio/luminos/CoachmarkConfig; + public static synthetic fun copy-I2SBm6M$default (Lio/luminos/CoachmarkConfig;FFFFFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZIILjava/lang/Object;)Lio/luminos/CoachmarkConfig; public fun equals (Ljava/lang/Object;)Z public final fun getBackPressBehavior ()Lio/luminos/BackPressBehavior; public final fun getConnectorAnimationDuration ()I @@ -121,6 +126,9 @@ public final class io/luminos/CoachmarkConfig { public final fun getConnectorArrowSize-D9Ej5fM ()F public final fun getConnectorCutoutGap-D9Ej5fM ()F public final fun getConnectorDotRadius-D9Ej5fM ()F + public final fun getConnectorTeardropLength-D9Ej5fM ()F + public final fun getConnectorTeardropTargetGap-D9Ej5fM ()F + public final fun getConnectorTeardropWidth-D9Ej5fM ()F public final fun getConnectorTooltipGap-D9Ej5fM ()F public final fun getCtaCornerRadius-D9Ej5fM ()F public final fun getCtaHorizontalArrangement ()Landroidx/compose/foundation/layout/Arrangement$Horizontal; @@ -331,6 +339,7 @@ public final class io/luminos/ConnectorEndStyle : java/lang/Enum { public static final field CUSTOM Lio/luminos/ConnectorEndStyle; public static final field DOT Lio/luminos/ConnectorEndStyle; public static final field NONE Lio/luminos/ConnectorEndStyle; + public static final field TEARDROP Lio/luminos/ConnectorEndStyle; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/luminos/ConnectorEndStyle; public static fun values ()[Lio/luminos/ConnectorEndStyle; @@ -342,6 +351,7 @@ public final class io/luminos/ConnectorStyle : java/lang/Enum { public static final field DIRECT Lio/luminos/ConnectorStyle; public static final field ELBOW Lio/luminos/ConnectorStyle; public static final field HORIZONTAL Lio/luminos/ConnectorStyle; + public static final field TEARDROP Lio/luminos/ConnectorStyle; public static final field VERTICAL Lio/luminos/ConnectorStyle; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/luminos/ConnectorStyle; diff --git a/lumen/api/jvm/lumen.api b/lumen/api/jvm/lumen.api index f7942da..be59e88 100644 --- a/lumen/api/jvm/lumen.api +++ b/lumen/api/jvm/lumen.api @@ -28,11 +28,12 @@ public final class io/luminos/CoachmarkAnalytics { public final class io/luminos/CoachmarkColors { public static final field $stable I - public synthetic fun (JJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (JJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-0d7_KjU ()J public final fun component10-0d7_KjU ()J public final fun component11-0d7_KjU ()J + public final fun component12-0d7_KjU ()J public final fun component2-0d7_KjU ()J public final fun component3-0d7_KjU ()J public final fun component4-0d7_KjU ()J @@ -41,10 +42,11 @@ public final class io/luminos/CoachmarkColors { public final fun component7-0d7_KjU ()J public final fun component8-0d7_KjU ()J public final fun component9-0d7_KjU ()J - public final fun copy-KpyCeHw (JJJJJJJJJJJ)Lio/luminos/CoachmarkColors; - public static synthetic fun copy-KpyCeHw$default (Lio/luminos/CoachmarkColors;JJJJJJJJJJJILjava/lang/Object;)Lio/luminos/CoachmarkColors; + public final fun copy-2qZNXz8 (JJJJJJJJJJJJ)Lio/luminos/CoachmarkColors; + public static synthetic fun copy-2qZNXz8$default (Lio/luminos/CoachmarkColors;JJJJJJJJJJJJILjava/lang/Object;)Lio/luminos/CoachmarkColors; public fun equals (Ljava/lang/Object;)Z public final fun getConnectorColor-0d7_KjU ()J + public final fun getConnectorTeardropColor-0d7_KjU ()J public final fun getCtaButtonColor-0d7_KjU ()J public final fun getCtaTextColor-0d7_KjU ()J public final fun getDescriptionColor-0d7_KjU ()J @@ -68,48 +70,51 @@ public final class io/luminos/CoachmarkColorsKt { public final class io/luminos/CoachmarkConfig { public static final field $stable I - public synthetic fun (FFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (FFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-D9Ej5fM ()F - public final fun component10 ()I - public final fun component11 ()I - public final fun component12 ()I - public final fun component13 ()Lio/luminos/ScrimOpacity; - public final fun component14 ()Lio/luminos/ScrimTapBehavior; - public final fun component15 ()Z - public final fun component16 ()Z - public final fun component17 ()Lio/luminos/BackPressBehavior; - public final fun component18 ()Lio/luminos/HighlightAnimation; - public final fun component19 ()I + public final fun component10-D9Ej5fM ()F + public final fun component11-D9Ej5fM ()F + public final fun component12-D9Ej5fM ()F + public final fun component13 ()I + public final fun component14 ()I + public final fun component15 ()I + public final fun component16 ()Lio/luminos/ScrimOpacity; + public final fun component17 ()Lio/luminos/ScrimTapBehavior; + public final fun component18 ()Z + public final fun component19 ()Z public final fun component2-D9Ej5fM ()F - public final fun component20 ()Z - public final fun component21 ()Ljava/lang/String; - public final fun component22 ()J + public final fun component20 ()Lio/luminos/BackPressBehavior; + public final fun component21 ()Lio/luminos/HighlightAnimation; + public final fun component22 ()I public final fun component23 ()Z - public final fun component24 ()J - public final fun component25 ()Ljava/lang/String; - public final fun component26 ()J - public final fun component27 ()Z - public final fun component28-D9Ej5fM ()F - public final fun component29-D9Ej5fM ()F + public final fun component24 ()Ljava/lang/String; + public final fun component25 ()J + public final fun component26 ()Z + public final fun component27 ()J + public final fun component28 ()Ljava/lang/String; + public final fun component29 ()J public final fun component3-D9Ej5fM ()F - public final fun component30-D9Ej5fM ()F + public final fun component30 ()Z public final fun component31-D9Ej5fM ()F - public final fun component32-e0LSkKk ()I - public final fun component33-e0LSkKk ()I - public final fun component34-e0LSkKk ()I + public final fun component32-D9Ej5fM ()F + public final fun component33-D9Ej5fM ()F + public final fun component34-D9Ej5fM ()F public final fun component35-e0LSkKk ()I public final fun component36-e0LSkKk ()I - public final fun component37 ()Landroidx/compose/foundation/layout/Arrangement$Horizontal; - public final fun component38 ()Z + public final fun component37-e0LSkKk ()I + public final fun component38-e0LSkKk ()I + public final fun component39-e0LSkKk ()I public final fun component4 ()F - public final fun component5 ()Lkotlin/jvm/functions/Function3; + public final fun component40 ()Landroidx/compose/foundation/layout/Arrangement$Horizontal; + public final fun component41 ()Z + public final fun component5-D9Ej5fM ()F public final fun component6-D9Ej5fM ()F public final fun component7-D9Ej5fM ()F - public final fun component8-D9Ej5fM ()F + public final fun component8 ()Lkotlin/jvm/functions/Function3; public final fun component9-D9Ej5fM ()F - public final fun copy-ZvfVXdg (FFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;Z)Lio/luminos/CoachmarkConfig; - public static synthetic fun copy-ZvfVXdg$default (Lio/luminos/CoachmarkConfig;FFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZIILjava/lang/Object;)Lio/luminos/CoachmarkConfig; + public final fun copy-I2SBm6M (FFFFFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;Z)Lio/luminos/CoachmarkConfig; + public static synthetic fun copy-I2SBm6M$default (Lio/luminos/CoachmarkConfig;FFFFFFFLkotlin/jvm/functions/Function3;FFFFIIILio/luminos/ScrimOpacity;Lio/luminos/ScrimTapBehavior;ZZLio/luminos/BackPressBehavior;Lio/luminos/HighlightAnimation;IZLjava/lang/String;JZJLjava/lang/String;JZFFFFIIIIILandroidx/compose/foundation/layout/Arrangement$Horizontal;ZIILjava/lang/Object;)Lio/luminos/CoachmarkConfig; public fun equals (Ljava/lang/Object;)Z public final fun getBackPressBehavior ()Lio/luminos/BackPressBehavior; public final fun getConnectorAnimationDuration ()I @@ -117,6 +122,9 @@ public final class io/luminos/CoachmarkConfig { public final fun getConnectorArrowSize-D9Ej5fM ()F public final fun getConnectorCutoutGap-D9Ej5fM ()F public final fun getConnectorDotRadius-D9Ej5fM ()F + public final fun getConnectorTeardropLength-D9Ej5fM ()F + public final fun getConnectorTeardropTargetGap-D9Ej5fM ()F + public final fun getConnectorTeardropWidth-D9Ej5fM ()F public final fun getConnectorTooltipGap-D9Ej5fM ()F public final fun getCtaCornerRadius-D9Ej5fM ()F public final fun getCtaHorizontalArrangement ()Landroidx/compose/foundation/layout/Arrangement$Horizontal; @@ -327,6 +335,7 @@ public final class io/luminos/ConnectorEndStyle : java/lang/Enum { public static final field CUSTOM Lio/luminos/ConnectorEndStyle; public static final field DOT Lio/luminos/ConnectorEndStyle; public static final field NONE Lio/luminos/ConnectorEndStyle; + public static final field TEARDROP Lio/luminos/ConnectorEndStyle; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/luminos/ConnectorEndStyle; public static fun values ()[Lio/luminos/ConnectorEndStyle; @@ -338,6 +347,7 @@ public final class io/luminos/ConnectorStyle : java/lang/Enum { public static final field DIRECT Lio/luminos/ConnectorStyle; public static final field ELBOW Lio/luminos/ConnectorStyle; public static final field HORIZONTAL Lio/luminos/ConnectorStyle; + public static final field TEARDROP Lio/luminos/ConnectorStyle; public static final field VERTICAL Lio/luminos/ConnectorStyle; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/luminos/ConnectorStyle; diff --git a/lumen/build.gradle.kts b/lumen/build.gradle.kts index 3edea9a..a11daa5 100644 --- a/lumen/build.gradle.kts +++ b/lumen/build.gradle.kts @@ -106,7 +106,7 @@ android { } } -val libraryVersion = "1.0.0-beta15" +val libraryVersion = "1.0.0-beta16" val libraryGroup = "io.github.aldefy" val libraryArtifact = "lumen" diff --git a/lumen/src/androidUnitTest/kotlin/io/luminos/screenshot/CoachmarkScrimScreenshotTest.kt b/lumen/src/androidUnitTest/kotlin/io/luminos/screenshot/CoachmarkScrimScreenshotTest.kt index 705c30e..841ddc7 100644 --- a/lumen/src/androidUnitTest/kotlin/io/luminos/screenshot/CoachmarkScrimScreenshotTest.kt +++ b/lumen/src/androidUnitTest/kotlin/io/luminos/screenshot/CoachmarkScrimScreenshotTest.kt @@ -12,6 +12,7 @@ import io.luminos.CoachmarkConfig import io.luminos.CoachmarkController import io.luminos.CoachmarkScrim import io.luminos.CoachmarkTarget +import io.luminos.ConnectorEndStyle import io.luminos.ConnectorStyle import io.luminos.CutoutShape import io.luminos.DarkCoachmarkColors @@ -184,6 +185,38 @@ class CoachmarkScrimScreenshotTest { ) } + @Test + fun scrim_teardropConnector() { + showScrimAndCapture( + target = CoachmarkTarget( + id = "teardrop", + bounds = targetBounds, + shape = CutoutShape.Circle(), + title = "Teardrop Connector", + description = "A rounded teardrop nub anchored on the tooltip edge, no connector line.", + connectorStyle = ConnectorStyle.TEARDROP, + ), + config = noAnimConfig.copy(showTooltipCard = true), + filePath = "src/androidUnitTest/snapshots/scrim_teardropConnector.png", + ) + } + + @Test + fun scrim_teardropEndStyle() { + showScrimAndCapture( + target = CoachmarkTarget( + id = "teardropEnd", + bounds = targetBounds, + shape = CutoutShape.Circle(), + title = "Teardrop Endpoint", + description = "A rounded petal shape at the end of a direct connector line.", + connectorStyle = ConnectorStyle.DIRECT, + connectorEndStyle = ConnectorEndStyle.TEARDROP, + ), + filePath = "src/androidUnitTest/snapshots/scrim_teardropEndStyle.png", + ) + } + @Test fun scrim_tooltipAbove() { val lowerTarget = Rect(left = 150f, top = 600f, right = 250f, bottom = 660f) diff --git a/lumen/src/androidUnitTest/snapshots/scrim_teardropConnector.png b/lumen/src/androidUnitTest/snapshots/scrim_teardropConnector.png new file mode 100644 index 0000000..d1fb17e Binary files /dev/null and b/lumen/src/androidUnitTest/snapshots/scrim_teardropConnector.png differ diff --git a/lumen/src/androidUnitTest/snapshots/scrim_teardropEndStyle.png b/lumen/src/androidUnitTest/snapshots/scrim_teardropEndStyle.png new file mode 100644 index 0000000..4d68291 Binary files /dev/null and b/lumen/src/androidUnitTest/snapshots/scrim_teardropEndStyle.png differ diff --git a/lumen/src/commonMain/kotlin/io/luminos/CoachmarkColors.kt b/lumen/src/commonMain/kotlin/io/luminos/CoachmarkColors.kt index 119255e..52a0b39 100644 --- a/lumen/src/commonMain/kotlin/io/luminos/CoachmarkColors.kt +++ b/lumen/src/commonMain/kotlin/io/luminos/CoachmarkColors.kt @@ -17,6 +17,12 @@ data class CoachmarkColors( val strokeColor: Color = Color.White, /** Color for the connector line from cutout to tooltip */ val connectorColor: Color = Color.White, + /** + * Color for the [ConnectorStyle.TEARDROP] nub and [ConnectorEndStyle.TEARDROP] endpoint. + * [Color.Unspecified] (default) falls back to [connectorColor]. Set this to match your + * tooltip card background (e.g. [tooltipCardColor]) for a seamless attached-tail look. + */ + val connectorTeardropColor: Color = Color.Unspecified, /** Background color for the tooltip card */ val tooltipBackground: Color = Color.White, /** Background color for the tooltip card when showCard is enabled */ diff --git a/lumen/src/commonMain/kotlin/io/luminos/CoachmarkScrim.kt b/lumen/src/commonMain/kotlin/io/luminos/CoachmarkScrim.kt index fdddefb..5fdb887 100644 --- a/lumen/src/commonMain/kotlin/io/luminos/CoachmarkScrim.kt +++ b/lumen/src/commonMain/kotlin/io/luminos/CoachmarkScrim.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.style.TextAlign @@ -175,6 +176,21 @@ data class CoachmarkConfig( val connectorArrowSize: Dp = 10.dp, /** Half-angle of arrowhead wings in degrees */ val connectorArrowAngle: Float = 30f, + /** + * Height (base to peak) of the teardrop shape used by [ConnectorEndStyle.TEARDROP] and + * [ConnectorStyle.TEARDROP]. It's a low, wide hill, not a tall spike — keep this smaller + * than [connectorTeardropWidth]. + */ + val connectorTeardropLength: Dp = 11.dp, + /** Width of the flat base of the teardrop shape. */ + val connectorTeardropWidth: Dp = 24.dp, + /** + * For [ConnectorStyle.TEARDROP] only: gap left between the nub's tip and the target's + * cutout edge, so the tail points at the target without touching it. The tooltip is + * positioned so the total cutout-to-card distance equals [connectorTeardropLength] plus + * this value. + */ + val connectorTeardropTargetGap: Dp = 8.dp, /** Custom endpoint DrawScope lambda for ConnectorEndStyle.CUSTOM */ val customConnectorEnd: (DrawScope.(center: Offset, angle: Float) -> Unit)? = null, /** Minimum distance from screen edges for the tooltip */ @@ -547,9 +563,12 @@ private fun CoachmarkScrimContent( } else { 0f } + val teardropLengthPx = with(density) { config.connectorTeardropLength.toPx() } + val teardropWidthPx = with(density) { config.connectorTeardropWidth.toPx() } + val teardropTargetGapPx = with(density) { config.connectorTeardropTargetGap.toPx() } val tooltipPosition = - remember(target, screenSize, tooltipSize, connectorLengthPx) { + remember(target, screenSize, tooltipSize, connectorLengthPx, teardropLengthPx, teardropTargetGapPx) { calculateTooltipPosition( targetBounds = target.bounds, tooltipSize = tooltipSize, @@ -559,6 +578,8 @@ private fun CoachmarkScrimContent( gap = with(density) { config.tooltipGap.toPx() }, connectorLength = connectorLengthPx, connectorStyle = target.connectorStyle, + teardropLength = teardropLengthPx, + teardropTargetGap = teardropTargetGapPx, ) } @@ -575,7 +596,7 @@ private fun CoachmarkScrimContent( // When below: composed dot renders at the top of the tooltip (beside the title). // When above: composed dot renders at the bottom of the tooltip. val isTooltipBelow = if (tooltipSize != IntSize.Zero) { - tooltipPosition.y > target.bounds.bottom + tooltipPosition.y >= target.bounds.bottom } else { true } @@ -596,7 +617,7 @@ private fun CoachmarkScrimContent( (resolvedConnectorStyle == ConnectorStyle.VERTICAL || resolvedConnectorStyle == ConnectorStyle.ELBOW) val connectorPathData = - remember(target, tooltipPosition, tooltipSize, density, connectorTooltipGapPx, strokeWidthPx, connectorDotRadiusPx, connectorCutoutGapPx, isInlineTitleActive, inlineDotCenter) { + remember(target, tooltipPosition, tooltipSize, density, connectorTooltipGapPx, strokeWidthPx, connectorDotRadiusPx, connectorCutoutGapPx, isInlineTitleActive, inlineDotCenter, teardropLengthPx) { calculateConnectorPath( target = target, tooltipPosition = tooltipPosition, @@ -608,6 +629,8 @@ private fun CoachmarkScrimContent( connectorCutoutGap = connectorCutoutGapPx, isInlineTitleActive = isInlineTitleActive, inlineDotCenter = inlineDotCenter, + teardropLength = teardropLengthPx, + tooltipMargin = tooltipMarginPx, ) } @@ -878,6 +901,11 @@ private fun CoachmarkScrimContent( val arrowSizePx = with(density) { config.connectorArrowSize.toPx() } val arrowHalfAngleRad = (config.connectorArrowAngle * PI / 180f).toFloat() val effectiveEndStyle = if (isInlineTitleActive) ConnectorEndStyle.NONE else target.connectorEndStyle + val teardropColor = if (colors.connectorTeardropColor.isSpecified) { + colors.connectorTeardropColor + } else { + colors.connectorColor + } when (connectorPathData) { is ConnectorPathData.Segments -> { if (connectorPathData.points.isNotEmpty()) { @@ -891,6 +919,9 @@ private fun CoachmarkScrimContent( arrowSize = arrowSizePx, arrowHalfAngle = arrowHalfAngleRad, customEnd = config.customConnectorEnd, + teardropLength = teardropLengthPx, + teardropWidth = teardropWidthPx, + teardropColor = teardropColor, ) } } @@ -905,6 +936,17 @@ private fun CoachmarkScrimContent( arrowSize = arrowSizePx, arrowHalfAngle = arrowHalfAngleRad, customEnd = config.customConnectorEnd, + teardropLength = teardropLengthPx, + teardropWidth = teardropWidthPx, + teardropColor = teardropColor, + ) + } + is ConnectorPathData.Teardrop -> { + drawConnectorTeardropNub( + teardrop = connectorPathData, + progress = connectorProgress.value, + color = teardropColor, + width = teardropWidthPx, ) } } @@ -1268,6 +1310,9 @@ private fun DrawScope.drawConnectorPath( arrowSize: Float = 0f, arrowHalfAngle: Float = 0f, customEnd: (DrawScope.(Offset, Float) -> Unit)? = null, + teardropLength: Float = 0f, + teardropWidth: Float = 0f, + teardropColor: Color = Color.Unspecified, ) { if (points.size < 2) return @@ -1308,7 +1353,7 @@ private fun DrawScope.drawConnectorPath( if (progress >= 1f) { val angle = calculateConnectorAngle(points) - drawConnectorEndpoint(endStyle, color, dotRadius, points.last(), angle, arrowSize, arrowHalfAngle, customEnd) + drawConnectorEndpoint(endStyle, color, dotRadius, points.last(), angle, arrowSize, arrowHalfAngle, customEnd, teardropLength, teardropWidth, teardropColor) } } @@ -1322,6 +1367,9 @@ private fun DrawScope.drawConnectorEndpoint( arrowSize: Float, arrowHalfAngle: Float, customEnd: (DrawScope.(Offset, Float) -> Unit)?, + teardropLength: Float = 0f, + teardropWidth: Float = 0f, + teardropColor: Color = Color.Unspecified, ) { when (endStyle) { ConnectorEndStyle.DOT -> { @@ -1334,6 +1382,11 @@ private fun DrawScope.drawConnectorEndpoint( ConnectorEndStyle.CUSTOM -> { customEnd?.invoke(this, center, angle) } + ConnectorEndStyle.TEARDROP -> { + val base = Offset(center.x - teardropLength * cos(angle), center.y - teardropLength * sin(angle)) + val resolvedColor = if (teardropColor.isSpecified) teardropColor else color + drawTeardrop(tip = center, base = base, width = teardropWidth, color = resolvedColor) + } } } @@ -1363,6 +1416,51 @@ private fun DrawScope.drawArrowHead( drawPath(path = path, color = color) } +/** + * Builds a smooth teardrop "hill" outline: a flat base of the given [width] centered on [base], + * rising via two mirrored S-curves to a single rounded peak at [tip]. Used for + * [ConnectorEndStyle.TEARDROP] and the [ConnectorStyle.TEARDROP] nub. The flat base is meant to + * sit flush against (and be partly hidden behind) the tooltip card or connector line it grows + * out of, so only the rounded hill silhouette reads as visible. + */ +private fun teardropPath(tip: Offset, base: Offset, width: Float): Path { + val dirX = tip.x - base.x + val dirY = tip.y - base.y + val length = kotlin.math.sqrt(dirX * dirX + dirY * dirY) + if (length < 0.01f) return Path() + + val ux = dirX / length + val uy = dirY / length + val perpX = -uy + val perpY = ux + val halfWidth = width / 2f + + val baseLeft = Offset(base.x - perpX * halfWidth, base.y - perpY * halfWidth) + val baseRight = Offset(base.x + perpX * halfWidth, base.y + perpY * halfWidth) + + // Control points a quarter-width in from each base corner, at that corner's own height + // (flat tangent at the base) or at the peak's height (flat tangent at the peak). + val quarterWidth = halfWidth / 2f + val leftNearBase = Offset(baseLeft.x + perpX * quarterWidth, baseLeft.y + perpY * quarterWidth) + val leftNearPeak = Offset(tip.x - perpX * quarterWidth, tip.y - perpY * quarterWidth) + val rightNearPeak = Offset(tip.x + perpX * quarterWidth, tip.y + perpY * quarterWidth) + val rightNearBase = Offset(baseRight.x - perpX * quarterWidth, baseRight.y - perpY * quarterWidth) + + return Path().apply { + moveTo(baseLeft.x, baseLeft.y) + cubicTo(leftNearBase.x, leftNearBase.y, leftNearPeak.x, leftNearPeak.y, tip.x, tip.y) + cubicTo(rightNearPeak.x, rightNearPeak.y, rightNearBase.x, rightNearBase.y, baseRight.x, baseRight.y) + lineTo(baseLeft.x, baseLeft.y) + close() + } +} + +/** Draws a smooth teardrop "hill" shape from [base] (flat, hidden) to [tip] (rounded peak). */ +private fun DrawScope.drawTeardrop(tip: Offset, base: Offset, width: Float, color: Color) { + val path = teardropPath(tip, base, width) + drawPath(path = path, color = color) +} + /** Draws a Bezier curve connector with progressive animation. */ private fun DrawScope.drawConnectorCurve( curve: ConnectorPathData.Curve, @@ -1374,6 +1472,9 @@ private fun DrawScope.drawConnectorCurve( arrowSize: Float = 0f, arrowHalfAngle: Float = 0f, customEnd: (DrawScope.(Offset, Float) -> Unit)? = null, + teardropLength: Float = 0f, + teardropWidth: Float = 0f, + teardropColor: Color = Color.Unspecified, ) { val steps = 50 val totalSteps = (steps * progress).toInt().coerceAtLeast(1) @@ -1408,10 +1509,24 @@ private fun DrawScope.drawConnectorCurve( points.last().y - points[points.size - 2].y, points.last().x - points[points.size - 2].x, ) - drawConnectorEndpoint(endStyle, color, dotRadius, points.last(), angle, arrowSize, arrowHalfAngle, customEnd) + drawConnectorEndpoint(endStyle, color, dotRadius, points.last(), angle, arrowSize, arrowHalfAngle, customEnd, teardropLength, teardropWidth, teardropColor) } } +/** Draws the [ConnectorStyle.TEARDROP] nub, growing from [ConnectorPathData.Teardrop.base] to its tip as [progress] advances. */ +private fun DrawScope.drawConnectorTeardropNub( + teardrop: ConnectorPathData.Teardrop, + progress: Float, + color: Color, + width: Float, +) { + val animatedTip = Offset( + x = teardrop.base.x + (teardrop.tip.x - teardrop.base.x) * progress, + y = teardrop.base.y + (teardrop.tip.y - teardrop.base.y) * progress, + ) + drawTeardrop(tip = animatedTip, base = teardrop.base, width = width, color = color) +} + private fun calculateTooltipPosition( targetBounds: Rect, tooltipSize: IntSize, @@ -1421,6 +1536,8 @@ private fun calculateTooltipPosition( gap: Float, connectorLength: Float, connectorStyle: ConnectorStyle = ConnectorStyle.AUTO, + teardropLength: Float = 0f, + teardropTargetGap: Float = 0f, ): Offset { if (screenSize == IntSize.Zero) { return Offset.Zero @@ -1430,8 +1547,19 @@ private fun calculateTooltipPosition( val targetCenterX = targetBounds.center.x val targetCenterY = targetBounds.center.y - // Use connectorLength if specified, otherwise use the default gap - val effectiveGap = if (connectorLength > 0f) connectorLength else gap + // TEARDROP replaces the connector line with a nub that spans from the visible card edge + // toward the target, leaving teardropTargetGap of clearance before the target — so the + // tooltip sits just far enough away for the nub plus that gap to bridge the distance. + // connectorLength is ignored. The visible card sits `margin` inside the measured tooltip + // box (TooltipContainer pads its content by tooltipMargin), so that inset is subtracted + // back out here to keep the total cutout-to-card distance equal to + // teardropLength + teardropTargetGap. + val effectiveGap = when { + connectorStyle == ConnectorStyle.TEARDROP -> + (teardropLength + teardropTargetGap - margin).coerceAtLeast(0f) + connectorLength > 0f -> connectorLength + else -> gap + } // For HORIZONTAL connector style, position tooltip beside the target if (connectorStyle == ConnectorStyle.HORIZONTAL) { @@ -1476,6 +1604,9 @@ private fun calculateTooltipPosition( private sealed interface ConnectorPathData { data class Segments(val points: List) : ConnectorPathData data class Curve(val start: Offset, val control: Offset, val end: Offset) : ConnectorPathData + + /** [base] anchors on the tooltip edge (partially hidden behind the card); [tip] points at the target. */ + data class Teardrop(val base: Offset, val tip: Offset) : ConnectorPathData } private fun resolveConnectorStyle( @@ -1508,6 +1639,8 @@ private fun calculateConnectorPath( connectorCutoutGap: Float = 0f, isInlineTitleActive: Boolean = false, inlineDotCenter: Offset = Offset.Unspecified, + teardropLength: Float = 0f, + tooltipMargin: Float = 0f, ): ConnectorPathData { if (tooltipSize == IntSize.Zero) { return ConnectorPathData.Segments(emptyList()) @@ -1562,7 +1695,7 @@ private fun calculateConnectorPath( val tooltipCenterX = tooltipPosition.x + tooltipSize.width / 2f val tooltipCenterY = tooltipPosition.y + tooltipSize.height / 2f - val isTooltipBelow = tooltipPosition.y > targetBounds.bottom + val isTooltipBelow = tooltipPosition.y >= targetBounds.bottom val resolvedStyle = resolveConnectorStyle( connectorStyle = connectorStyle, @@ -1703,6 +1836,37 @@ private fun calculateConnectorPath( val controlPoint = Offset(targetCenter.x, (cutoutEdgePoint.y + endPoint.y) / 2f) ConnectorPathData.Curve(cutoutEdgePoint, controlPoint, endPoint) } + + ConnectorStyle.TEARDROP -> { + // Anchor on the *visible* card's top/bottom edge — the measured tooltip box is + // padded by tooltipMargin on all sides (TooltipContainer), so the actual card sits + // inset from tooltipPosition/tooltipSize by that amount. Clamped away from the + // card's rounded corners. + val cardLeft = tooltipPosition.x + tooltipMargin + val cardRight = tooltipPosition.x + tooltipSize.width - tooltipMargin + val anchorX = targetCenter.x.coerceIn( + cardLeft + 20f, + (cardRight - 20f).coerceAtLeast(cardLeft + 20f), + ) + val anchorY = if (isTooltipBelow) { + tooltipPosition.y + tooltipMargin + } else { + tooltipPosition.y + tooltipSize.height - tooltipMargin + } + val base = Offset(anchorX, anchorY) + + val dx = targetCenter.x - base.x + val dy = targetCenter.y - base.y + val distance = kotlin.math.sqrt(dx * dx + dy * dy) + val (normalizedDx, normalizedDy) = if (distance < 1f) { + 0f to (if (isTooltipBelow) -1f else 1f) + } else { + (dx / distance) to (dy / distance) + } + + val tip = Offset(base.x + normalizedDx * teardropLength, base.y + normalizedDy * teardropLength) + ConnectorPathData.Teardrop(base = base, tip = tip) + } } } diff --git a/lumen/src/commonMain/kotlin/io/luminos/CoachmarkTarget.kt b/lumen/src/commonMain/kotlin/io/luminos/CoachmarkTarget.kt index f0992e9..12e2a99 100644 --- a/lumen/src/commonMain/kotlin/io/luminos/CoachmarkTarget.kt +++ b/lumen/src/commonMain/kotlin/io/luminos/CoachmarkTarget.kt @@ -177,6 +177,21 @@ enum class ConnectorStyle { /** Smooth curved connector using a quadratic Bezier curve. */ CURVED, + + /** + * Rounded teardrop-shaped nub anchored directly on the tooltip edge and pointing + * toward the target, like a classic speech-bubble tail — no connecting line is drawn. + * + * [CoachmarkTarget.connectorLength] is ignored for this style; use + * [CoachmarkConfig.connectorTeardropLength] and [CoachmarkConfig.connectorTeardropWidth] + * to size the nub, and [CoachmarkConfig.connectorTeardropTargetGap] to control the gap + * left between the nub's tip and the target. [CoachmarkTarget.connectorEndStyle] is also + * ignored, since the nub itself is both the connector and its endpoint. + * + * Tip: set [CoachmarkColors.connectorTeardropColor] to match your tooltip card background + * for a seamless attached-tail look, without affecting other connectors' color. + */ + TEARDROP, } /** @@ -205,6 +220,12 @@ enum class ConnectorEndStyle { NONE, /** Custom rendering via CoachmarkConfig.customConnectorEnd lambda. */ CUSTOM, + /** + * Rounded teardrop/petal shape pointing toward the tooltip, with smooth curved + * sides instead of the sharp edges of [ARROW]. Sized via + * [CoachmarkConfig.connectorTeardropLength] and [CoachmarkConfig.connectorTeardropWidth]. + */ + TEARDROP, } /** diff --git a/lumen/src/commonTest/kotlin/io/luminos/CoachmarkTargetTest.kt b/lumen/src/commonTest/kotlin/io/luminos/CoachmarkTargetTest.kt index f598093..b21fa98 100644 --- a/lumen/src/commonTest/kotlin/io/luminos/CoachmarkTargetTest.kt +++ b/lumen/src/commonTest/kotlin/io/luminos/CoachmarkTargetTest.kt @@ -153,13 +153,14 @@ class CoachmarkTargetTest { @Test fun connectorStyle_values() { val values = ConnectorStyle.entries - assertEquals(6, values.size) + assertEquals(7, values.size) assertTrue(values.contains(ConnectorStyle.AUTO)) assertTrue(values.contains(ConnectorStyle.DIRECT)) assertTrue(values.contains(ConnectorStyle.HORIZONTAL)) assertTrue(values.contains(ConnectorStyle.VERTICAL)) assertTrue(values.contains(ConnectorStyle.ELBOW)) assertTrue(values.contains(ConnectorStyle.CURVED)) + assertTrue(values.contains(ConnectorStyle.TEARDROP)) } @Test @@ -171,11 +172,12 @@ class CoachmarkTargetTest { @Test fun connectorEndStyle_values() { val values = ConnectorEndStyle.entries - assertEquals(4, values.size) + assertEquals(5, values.size) assertTrue(values.contains(ConnectorEndStyle.DOT)) assertTrue(values.contains(ConnectorEndStyle.ARROW)) assertTrue(values.contains(ConnectorEndStyle.NONE)) assertTrue(values.contains(ConnectorEndStyle.CUSTOM)) + assertTrue(values.contains(ConnectorEndStyle.TEARDROP)) } // ── TargetTapBehavior ────────────────────────────────────────────────── diff --git a/sample/src/commonMain/kotlin/com/aditlal/sample/examples/ConnectorsExample.kt b/sample/src/commonMain/kotlin/com/aditlal/sample/examples/ConnectorsExample.kt index 27d8ecd..97e34a5 100644 --- a/sample/src/commonMain/kotlin/com/aditlal/sample/examples/ConnectorsExample.kt +++ b/sample/src/commonMain/kotlin/com/aditlal/sample/examples/ConnectorsExample.kt @@ -69,6 +69,9 @@ fun ConnectorsExample( showSkipButton = true, skipButtonText = "Skip", scrimTapBehavior = ScrimTapBehavior.NONE, + // TEARDROP connector style anchors its nub directly on the tooltip card edge, + // so it reads best with the card background enabled. + showTooltipCard = true, customConnectorEnd = { center, angle -> // Diamond shape endpoint val size = 10f @@ -134,7 +137,7 @@ fun ConnectorsExample( Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Eight different ways to connect tooltips to targets", + text = "Ten different ways to connect tooltips to targets", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -289,6 +292,24 @@ fun ConnectorsExample( connectorLength = 90.dp, ctaText = "Got it!", ), + CoachmarkTarget( + id = "vertical", + title = "Teardrop Connector", + description = "A rounded teardrop nub grows straight out of the tooltip card toward the target — no connector line at all, like a classic speech-bubble tail.", + shape = CutoutShape.Circle(radiusPadding = 10.dp), + connectorStyle = ConnectorStyle.TEARDROP, + ctaText = "Next", + ), + CoachmarkTarget( + id = "direct", + title = "Teardrop Endpoint", + description = "Same idea, applied to the connector line's endpoint instead: a rounded petal shape with smooth curved sides, in place of the sharp arrowhead.", + shape = CutoutShape.Circle(radiusPadding = 10.dp), + connectorStyle = ConnectorStyle.DIRECT, + connectorEndStyle = ConnectorEndStyle.TEARDROP, + connectorLength = 80.dp, + ctaText = "Got it!", + ), ) ) showSequence = false