A unified onboarding step, with a full-bleed adaptive hero #40

Closed
opened 2026-08-18 22:47:09 -05:00 by null · 1 comment
Owner

Seven hand-rolled step layouts become one component, and the illustration becomes a hero that spans the screen.

What is true now

Every step in app/src/main/kotlin/dev/privacyllc/period/feature/onboarding/OnboardingScreen.kt hand-rolls the same shape — Spacer → Illustration(size) → Spacer → Heading → controls — with three different spacer rhythms (32/32 on Welcome, 24/24 on four steps, 16/16 on steps 3 and 6) and a reason recorded for only two of the divergences.

There is no shared layout component anywhere in core/designsystem: no screen scaffold, no spacing constants. Spacing is hard-coded dp literals in every feature file. The only reusable objects are IllustrationDefaults and MarkerDefaults, both art-specific.

Illustration() at core/designsystem/src/main/kotlin/dev/privacyllc/period/designsystem/art/Illustrations.kt:149-174 applies .height(size), ContentScale.FillHeight and .clip(RoundedCornerShape(18.dp)).

What it costs

Seven copies of one layout is seven places for the rhythm to drift, and it already has. More immediately, the illustration cannot be made a hero while every step positions it independently.

What to do

  1. One OnboardingStep(artHeight, art, title, body, content) composable. The seven steps become declarations. artHeight stays a per-step parameter — see the traps.
  2. Illustration() becomes the hero: fillMaxWidth(), ContentScale.Crop, and .clip(...) removed along with IllustrationDefaults.CornerRadius. Rewrite the KDoc rather than deleting it — record that the corners went away when the art went full-bleed, so nobody restores them.
  3. Adaptive height. Wrap in BoxWithConstraints and use min(target, maxHeight * 0.32f).
  4. Bottom fade. A Brush.verticalGradient from Color.Transparent to MaterialTheme.colorScheme.background over the bottom ~30%, inside a Box. The theme token, never a hard-coded colour.
  5. Heights: Welcome 280, LastPeriod 280, PeriodEnd 200, PreviousHistory 240, PrivacyPromise 280, NotificationPrivacy 200, FirstForecast 240.
  6. Typography: Heading()'s title moves from headlineMedium (28 sp) to headlineLarge (32 sp). Body stays bodyLarge.

Traps

  • The hero must escape the screen's padding, and Compose has no negative padding. The scrolling Column applies a uniform .padding(24.dp), so nothing inside it can reach the screen edge. Change it to vertical = 24.dp and have the step component apply horizontal = 24.dp to everything except the hero. Check the message Card and the Back TextButton still get theirs.
  • Steps 3 and 6 are deliberately smaller and must stay smaller. They carry four and three controls below the art, and their current 104 dp was chosen so the primary button stays reachable without scrolling — verified at font scale 2.0 and recorded in docs/qa/ClaudeQACoverage.md. Keep the comments explaining why.
  • Landscape is 914 × 411 dp, where a fixed 280 dp hero would be 68% of the screen. Nothing in this app uses WindowSizeClass or LocalConfiguration, and onboarding does not lock orientation. The adaptive cap is what makes rotation survivable.
  • Do not touch TodayScreen.kt's EmptyStateIllustration or InsightsScreen.kt's LearningIllustration. Those are in-page empty states, not heroes, and design rule 1 says nothing may compete with the forecast number.
  • Landing this before the new artwork is fine — it will simply look soft until then.

Verify: all seven steps driven by hand on PeriodQA and PeriodMinSdk26, in both themes, at font scales 1.0, 1.3 and 2.0, with the primary button reachable on every step; plus one landscape pass on each device showing the hero shrinking rather than pushing the button off screen.

Seven hand-rolled step layouts become one component, and the illustration becomes a hero that spans the screen. ## What is true now Every step in `app/src/main/kotlin/dev/privacyllc/period/feature/onboarding/OnboardingScreen.kt` hand-rolls the same shape — `Spacer → Illustration(size) → Spacer → Heading → controls` — with **three different spacer rhythms** (32/32 on Welcome, 24/24 on four steps, 16/16 on steps 3 and 6) and a reason recorded for only two of the divergences. There is **no shared layout component anywhere in `core/designsystem`**: no screen scaffold, no spacing constants. Spacing is hard-coded `dp` literals in every feature file. The only reusable objects are `IllustrationDefaults` and `MarkerDefaults`, both art-specific. `Illustration()` at `core/designsystem/src/main/kotlin/dev/privacyllc/period/designsystem/art/Illustrations.kt:149-174` applies `.height(size)`, `ContentScale.FillHeight` and `.clip(RoundedCornerShape(18.dp))`. ## What it costs Seven copies of one layout is seven places for the rhythm to drift, and it already has. More immediately, the illustration cannot be made a hero while every step positions it independently. ## What to do 1. **One `OnboardingStep(artHeight, art, title, body, content)` composable.** The seven steps become declarations. `artHeight` stays a **per-step parameter** — see the traps. 2. **`Illustration()` becomes the hero**: `fillMaxWidth()`, `ContentScale.Crop`, and **`.clip(...)` removed** along with `IllustrationDefaults.CornerRadius`. Rewrite the KDoc rather than deleting it — record that the corners went away when the art went full-bleed, so nobody restores them. 3. **Adaptive height.** Wrap in `BoxWithConstraints` and use `min(target, maxHeight * 0.32f)`. 4. **Bottom fade.** A `Brush.verticalGradient` from `Color.Transparent` to `MaterialTheme.colorScheme.background` over the bottom ~30%, inside a `Box`. The theme token, never a hard-coded colour. 5. **Heights**: Welcome 280, LastPeriod 280, PeriodEnd 200, PreviousHistory 240, PrivacyPromise 280, NotificationPrivacy 200, FirstForecast 240. 6. **Typography**: `Heading()`'s title moves from `headlineMedium` (28 sp) to `headlineLarge` (32 sp). Body stays `bodyLarge`. ## Traps - **The hero must escape the screen's padding, and Compose has no negative padding.** The scrolling `Column` applies a uniform `.padding(24.dp)`, so nothing inside it can reach the screen edge. Change it to `vertical = 24.dp` and have the step component apply `horizontal = 24.dp` to everything except the hero. Check the message `Card` and the `Back` `TextButton` still get theirs. - **Steps 3 and 6 are deliberately smaller and must stay smaller.** They carry four and three controls below the art, and their current 104 dp was chosen so the primary button stays reachable without scrolling — verified at font scale 2.0 and recorded in `docs/qa/ClaudeQACoverage.md`. Keep the comments explaining why. - **Landscape is 914 × 411 dp**, where a fixed 280 dp hero would be 68% of the screen. Nothing in this app uses `WindowSizeClass` or `LocalConfiguration`, and onboarding does not lock orientation. The adaptive cap is what makes rotation survivable. - **Do not touch `TodayScreen.kt`'s `EmptyStateIllustration` or `InsightsScreen.kt`'s `LearningIllustration`.** Those are in-page empty states, not heroes, and design rule 1 says nothing may compete with the forecast number. - Landing this before the new artwork is fine — it will simply look soft until then. Verify: all seven steps driven by hand on `PeriodQA` and `PeriodMinSdk26`, in both themes, at font scales 1.0, 1.3 and 2.0, with the primary button reachable on every step; plus one landscape pass on each device showing the hero shrinking rather than pushing the button off screen.
null added this to the Batch 08 — Polish milestone 2026-08-18 22:47:09 -05:00
null added the
P2
label 2026-08-18 22:47:09 -05:00
null closed this issue 2026-08-19 01:02:10 -05:00
Author
Owner

Shipped in ae27ef6. StepBody replaces seven hand-rolled step layouts; Illustration is full-bleed with ContentScale.Crop, no clip, and a theme-drawn bottom fade. Height caps at a third of the screen via LocalConfiguration.screenHeightDp — BoxWithConstraints cannot work here because a verticalScroll column measures children with an infinite height constraint. Screen padding moved off the scrolling column so the hero can reach the edges. Driven on PeriodMinSdk26: all steps, both themes, font scale 1.0 and 2.0, and landscape, where the cap holds the hero to 131dp instead of 280dp and the primary button stays visible.

Shipped in ae27ef6. StepBody replaces seven hand-rolled step layouts; Illustration is full-bleed with ContentScale.Crop, no clip, and a theme-drawn bottom fade. Height caps at a third of the screen via LocalConfiguration.screenHeightDp — BoxWithConstraints cannot work here because a verticalScroll column measures children with an infinite height constraint. Screen padding moved off the scrolling column so the hero can reach the edges. Driven on PeriodMinSdk26: all steps, both themes, font scale 1.0 and 2.0, and landscape, where the cap holds the hero to 131dp instead of 280dp and the primary button stays visible.
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#40
No description provided.