Skip to content

feat: speech-bubble tooltip shape, per-target CUSTOM connector, dark-theme contrast fix - #54

Merged
aldefy merged 4 commits into
mainfrom
feature/teardrop-via-customconnector
Aug 20, 2026
Merged

aldefy merged 4 commits into
mainfrom
feature/teardrop-via-customconnector

Conversation

@aldefy

@aldefy aldefy commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Summary

Re-scopes #51 (ConnectorStyle.TEARDROP) onto #53's extension points, and closes the real gap that surfaced while doing it: a true speech-bubble tooltip shape, not a separately-drawn nub.

  • CoachmarkConfig.tooltipShape + io.luminos.shapes.SpeechBubbleShape: the tail is carved into the tooltip card's own Shape outline — one continuous Path, no seam, follows the card's own background — instead of a connector-drawn nub sitting next to the card. CoachmarkConfig.tooltipTailInset reserves the extra layout space the tail needs.
  • ConnectorStyle.CUSTOM + CoachmarkTarget.customConnector: a per-target override on top of feat: composable/lambda extension points for tooltip, connector, and cutout rendering #53's sequence-wide CoachmarkConfig.customConnector, so one step in a sequence can differ from the rest (mirrors the existing ConnectorEndStyle.CUSTOM pattern). CUSTOM with no lambda anywhere falls back to a normal line rather than nothing.
  • Two real bugs found and fixed via on-device verification, not just visual inspection:
    1. Tail direction was inverted (isTooltipBelow=true incorrectly mapped to a bottom-edge tail).
    2. DarkCoachmarkColors/default CoachmarkColors() failed WCAG contrast: tooltipCardColor vs scrimColor measured 1.19:1–1.38:1 (need ≥3:1). The default's titleColor = Color.Black on its own dark card additionally failed text contrast outright (1.38:1, needs ≥4.5:1) — pre-existing, unrelated to this branch, previously undetected because no sample exercised showTooltipCard = true with plain defaults. Fixed both to #666666 + matching light text; verified ≥3.1:1 card-vs-scrim, ≥6.2:1 text-vs-card. LightCoachmarkColors untouched (was already 17.26:1).

Known limitation (documented, not silently worked around)

tooltipShape/SpeechBubbleShape only support a tail on the top or bottom edge. Verified live with a temporary HORIZONTAL-connector target: the line correctly points sideways while the tail sits on the bottom edge, unrelated to where the line points. Don't combine tooltipShape with side-positioned targets yet — left as a follow-up.

Sample

  • CustomRenderingExample: "Speech Bubble Tail" step replaces the earlier connector-nub demo.
  • LazyColumnExample: "Item Cards" step (a real whole-row target) now uses tooltipShape, matching a reference speech-bubble tooltip design end-to-end.

Test plan

  • ./gradlew :lumen:apiCheck — passes, dumps regenerated
  • ./gradlew :lumen:testDebugUnitTest :lumen:jvmTest — 232 + 201 tests, 0 failures
  • ./gradlew :lumen:compileKotlinMetadata :lumen:compileDebugKotlinAndroid :lumen:compileKotlinJvm :sample:compileDebugKotlinAndroid :sample:compileKotlinWasmJs — all green
  • On-device (Pixel 9 Pro Fold, Android 17): speech-bubble tail on an icon target, on a real LazyColumn row target, contrast fix visually confirmed, HORIZONTAL-connector limitation reproduced and documented
  • Bumped to 1.0.0-beta17, CHANGELOG updated

aldefy added 4 commits August 20, 2026 10:16
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.
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.
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.
CHANGELOG entry for the tooltipShape / per-target CUSTOM connector /
contrast-fix work in this branch.
@aldefy
aldefy merged commit 4d4288c into main Aug 20, 2026
6 checks passed
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