Skip to content

Add ConnectorStyle.TEARDROP and ConnectorEndStyle.TEARDROP - #51

Closed
paul-at-equal wants to merge 1 commit into
aldefy:mainfrom
paul-at-equal:feature/teardrop-connector-style
Closed

paul-at-equal wants to merge 1 commit into
aldefy:mainfrom
paul-at-equal:feature/teardrop-connector-style

Conversation

@paul-at-equal

Copy link
Copy Markdown
Contributor

Summary

  • Adds ConnectorStyle.TEARDROP: a rounded teardrop/speech-bubble-tail nub that grows straight out of the tooltip card edge toward the target, with no connector line at all.
  • Adds ConnectorEndStyle.TEARDROP: the same rounded shape used as a connector line endpoint, as a smoother alternative to ARROW.
  • New CoachmarkConfig knobs: connectorTeardropLength, connectorTeardropWidth (size the shape) and connectorTeardropTargetGap (gap left between the nub's tip and the target).
  • New 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.
  • Fixes a latent off-by-one in isTooltipBelow (> → >=) that flips the tooltip side when the computed gap makes the tooltip's edge land exactly on the target's edge — reachable via the new TEARDROP gap math, but a pre-existing edge case for any style.
  • Bumped to 1.0.0-beta16, updated CHANGELOG.md and the API dump.
  • Added two "Teardrop Connector" / "Teardrop Endpoint" demos to the sample app's Connector Styles screen, plus two new Roborazzi screenshot tests.

Design notes

The teardrop shape is a smooth two-cubic-Bezier "hill" (flat base, single rounded peak) rather than the classic circle+point raindrop — this was iterated against a reference speech-bubble tail design and verified pixel-by-pixel against a Figma export (24×11 default proportions).

Test plan

  • ./gradlew :lumen:verifyRoborazziDebug — 228/230 pass (2 pre-existing unrelated failures in CoachmarkTooltipScreenshotTest, not touched by this PR)
  • ./gradlew :lumen:jvmTest — all pass, including updated ConnectorStyle/ConnectorEndStyle entry-count assertions
  • ./gradlew :lumen:apiCheck — passes against the updated API dump
  • Compiles clean on JVM, Android, iOS (simulator arm64), and wasmJs targets
  • Manually verified both new styles on a physical Android device by stepping through the sample app's Connector Styles sequence

🤖 Generated with Claude Code

Adds a rounded teardrop/speech-bubble-tail shape as a new connector
option, requested to match a reference design where the tooltip's
tail is a smooth "hill" bump growing out of the card edge rather than
a thin line with a dot/arrow.

- ConnectorStyle.TEARDROP: a nub anchored directly on the tooltip
  card edge and pointing toward the target, with no connector line.
- ConnectorEndStyle.TEARDROP: the same rounded shape used as a line
  endpoint decoration, as a smoother alternative to ARROW.
- CoachmarkConfig.connectorTeardropLength / connectorTeardropWidth
  size the shape; connectorTeardropTargetGap controls the gap left
  between the nub's tip and the target so it doesn't touch it.
- CoachmarkColors.connectorTeardropColor lets the shape's color be
  set independently of connectorColor (e.g. matched to the tooltip
  card background for a seamless attached-tail look).

Also fixes a pre-existing off-by-one: isTooltipBelow used a strict
`>` comparison, which flipped to the wrong tooltip side whenever the
computed gap made the tooltip's edge land exactly on the target's
edge (reachable via the new TEARDROP gap math, but a latent bug for
any style).

Bumped to 1.0.0-beta16 and updated CHANGELOG.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@aldefy

aldefy commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Before merging this, I want to flag something and ask you to re-scope rather than have it land as-is.

The visual requirement is real — I checked it against the actual design (a speech-bubble tail on a coachmark tooltip, no connector line, tail colored to match the card so it reads as part of the card). Your ConnectorStyle.TEARDROP + ConnectorEndStyle.TEARDROP + connectorTeardropColor correctly reproduce that.

But it's 2 new sealed enum cases + 4 new CoachmarkConfig/CoachmarkColors fields in :lumen, for one visual idiom. CoachmarkConfig was already at 40 fields before this PR. Reviewing this alongside #52 against the design that motivated both showed the same pattern each time: a specific visual need becomes permanent library API surface instead of app code.

So I built and merged #53, which adds general extension points instead:

  • CoachmarkConfig.customConnector: DrawScope.(from: Offset, to: Offset, progress: Float) -> Unit — full control of the connector (line + endpoint), given the two anchors and reveal progress.
  • CoachmarkConfig.progressIndicator (composable slot) and CutoutShape.Custom (arbitrary cutout outline), for the same reason, on other axes.

I verified on-device (Pixel 9 Pro Fold) that a teardrop identical to yours — rounded speech-bubble tail, no line, colored to match the card — is about 15 lines inside customConnector, reusing tooltipCardColor the caller already owns instead of needing a new connectorTeardropColor.

Ask: could you rebase this onto main and re-express TEARDROP as a customConnector usage in the sample app (a new example, or added to ConnectorsExample) instead of new ConnectorStyle/ConnectorEndStyle cases? Happy to pair on this or take a pass myself if that's easier — let me know which you'd prefer. If it turns out teardrop tails become a common ask across several apps, we can always promote it into the library later from real usage.

For reference, the extension-point PR is #53 (merged) — CoachmarkConfig.kt/CoachmarkScrim.kt around customConnector and the sample's CustomRenderingExample.kt show the pattern.

@aldefy

aldefy commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Update: I ended up taking a pass at this myself — #54 is the re-scoped version, ready for review.

It reproduces the teardrop tail exactly (verified on-device, including matching it to the card color the way connectorTeardropColor did), but built it as a true speech-bubble tooltip shape — the tail is carved into the card's own outline as one continuous Shape, not a separately-drawn nub — plus a per-target ConnectorStyle.CUSTOM so a single sequence step can override the connector without a config-wide default. No new sealed enum cases in ConnectorStyle/ConnectorEndStyle, no new CoachmarkColors field.

It also turned up two real bugs along the way (tail-direction inversion, and a pre-existing WCAG contrast failure in the dark-theme card colors) — both fixed and in #54's diff.

No action needed from you unless you want to review #54 or have thoughts on the approach — happy to talk through it. Leaving this PR open for now rather than closing it outright, since it's your call whether to close it once #54 lands.

aldefy added a commit that referenced this pull request Aug 20, 2026
…theme contrast fix (#54)

* feat: per-target ConnectorStyle.CUSTOM, sample teardrop tail demo

Dry-run for re-scoping #51 (ConnectorStyle.TEARDROP) onto #53's
customConnector extension point.

Finding: config.customConnector from #53 is sequence-wide only, with no
per-target override. Paul's TEARDROP could mix a teardrop connector with
an elbow connector in the same sequence (per-target CoachmarkTarget field);
customConnector alone could not. This fixes that gap:

- ConnectorStyle.CUSTOM: new enum case, the generic gate for full-control
  connector rendering (mirrors the existing ConnectorEndStyle.CUSTOM
  pattern). resolveConnectorStyle() resolves it through the same heuristic
  as AUTO for geometry purposes, so the anchors/fallback path stay correct.
- CoachmarkTarget.customConnector: per-target override, takes priority
  over CoachmarkConfig.customConnector when both are set.
- CUSTOM with no lambda supplied anywhere falls back to drawing a normal
  connector line rather than nothing, verified via a new screenshot test.

Sample: added a "Teardrop Tail" step to ConnectorsExample using
ConnectorStyle.CUSTOM, and a drawTeardropTail() helper — Paul's original
"hill" silhouette (compared against a classic pointed teardrop and a true
geometric pear/raindrop shape; the hill was the right call for a tail meant
to read as continuous with a rounded card, not a separate sharp shape).
Verified on-device end to end.

Two new CoachmarkScrimScreenshotTest cases cover priority (target lambda
wins over config default) and fallback (CUSTOM with nothing supplied still
draws a line) behavior.

* feat: speech-bubble tooltip shape with tail carved into the card outline

Adds CoachmarkConfig.tooltipShape + tooltipTailInset: the tail is one
continuous outline with the card (via a Shape), not a separately-drawn
nub next to it — matches a real speech-bubble reference design where the
tail reads as part of the card, no seam.

- shapes/SpeechBubblePath.kt: SpeechBubbleShape + speechBubblePath(),
  following the existing StarPath/SquirclePath convention. The card body
  is sized to (measured size - tailInset) so the tail draws inside the
  Shape's own bounds rather than bumping past them, since clip() always
  bounds to the actual layout size.
- CoachmarkTooltip gains tailAnchorX/tooltipShape/tooltipTailInset params.
  A Spacer inside the Column reserves tooltipTailInset on the tail's side
  so the Column's measured size includes room for the shape's tail.
- Scrim computes tailAnchorX the same way the existing inline-dot-X code
  does: target center X relative to the tooltip's left edge.

Sample: CustomRenderingExample's step 2 now uses tooltipShape instead of
customConnector for the speech-bubble tail (customConnector kept only as
a no-op to suppress the built-in line, since ConnectorStyle has no NONE
case today - a real gap worth a follow-up).

Two bugs found and fixed against on-device verification, not just visual
inspection:

1. Tail direction was inverted (isTooltipBelow=true was mapped to
   tailOnBottom=true; it should be the opposite - tooltip below the
   target means the tail points UP, on the top edge, toward it).

2. tooltipCardColor in DarkCoachmarkColors and the default CoachmarkColors
   failed WCAG contrast against scrimColor: DarkCoachmarkColors measured
   1.19:1, default measured 1.38:1 (need >=3:1 for a UI boundary), because
   both defaulted to a near-black card over a near-black scrim. The
   default's titleColor = Color.Black on a dark card additionally failed
   text contrast outright (1.38:1, needs >=4.5:1) - that one predates this
   change and was previously undetected because the sample never exercised
   showTooltipCard=true with the plain default colors.

   Fixed by moving both dark-mode-adjacent card colors to #666666 and
   fixing the default's title/description colors to white/near-white to
   match its (now-legible) dark card. Verified: card-vs-scrim >=3.1:1,
   title-vs-card >=6.2:1, description-vs-card >=3.86:1 in both palettes.
   LightCoachmarkColors was already fine (17.26:1) and is untouched.

Re-recorded the 12 tooltip_* goldens affected by the card color change.
Full suite green: 232 Android + 201 JVM tests, apiCheck passes.

* feat(sample): speech-bubble tail on the LazyColumn whole-row target

Adds CoachmarkConfig.tooltipShape to the LazyColumn example's existing
"Item Cards" step (list_card), which already targets a whole row via
CutoutShape.RoundedRect. This is the closest existing target to the
reference speech-bubble tooltip design - a whole list row, tooltip above
it, tail carved into the card's own bottom edge pointing down at the row,
no connector line. Verified on-device against a real scrollable list.

Also confirmed and documented a real gap while building this: tooltipShape
only supports a top or bottom tail (SpeechBubbleShape has no left/right
case), and CoachmarkScrim always computes tailAnchorX from
target.bounds.center.x regardless of connector direction. Verified live
with a temporary HORIZONTAL-connector target: the line correctly points
sideways at the target while the tail sits meaninglessly on the bottom
edge, disconnected from what the line points at. Left as a known
limitation for a future PR rather than silently working around it -
tooltipShape should not be combined with HORIZONTAL-style targets today.

* chore: bump to 1.0.0-beta17

CHANGELOG entry for the tooltipShape / per-target CUSTOM connector /
contrast-fix work in this branch.
@aldefy

aldefy commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Closing as superseded — #54 (merged) ships the same teardrop tail effect as a general extension point instead of new enum cases: CoachmarkConfig.tooltipShape/SpeechBubbleShape for the speech-bubble tail carved into the card's own outline, and per-target ConnectorStyle.CUSTOM for the "no line, mixed with other steps" behavior TEARDROP had.

Your original design call — the tail should be a filled hill/dome, not a sharp pointed teardrop — was correct and is what #54 uses; I compared it against a couple of alternatives (a classic concave-waist tail, a true geometric raindrop) before landing back on your original shape.

No action needed on your end. Reopen this if you'd rather discuss the enum-based approach directly — happy to talk through it.

@aldefy aldefy closed this Aug 20, 2026
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.

2 participants