Onboarding renders as a fixed 292x600 card on Android, not a screen #49

Closed
opened 2026-07-30 14:10:58 +00:00 by aiko · 1 comment
Owner

Onboarding lays out as a fixed-size card, so on a real Android device it renders as a small
box floating in a large black screen. Observed on both target devices, measured from
screenshots rather than eyeballed.

src/renderer/src/components/receiver/onboarding-spec.ts:44-45:

export const SCREEN_WIDTH = '292px';
export const SCREEN_HEIGHT = '600px';

applied at src/renderer/src/components/receiver/onboarding.tsx:465-468:

width={SCREEN_WIDTH}
minHeight={SCREEN_HEIGHT}
maxWidth="100%"
margin="0 auto"

What it looks like

Pixel 7 Pro OnePlus Pad
Screen 1440×3120 2000×2800
Density 476 (DPR 2.975) 400 (DPR 2.5)
CSS viewport ~484×1049 800×1120
Card, measured 292×598 CSS px 292 CSS px wide (731 device px)
Share of width ~60% ~37%

The card is 292 px on both, so the larger the screen the worse it reads — on the tablet it
occupies just over a third of the width, with the rest empty. maxWidth="100%" only helps on a
viewport narrower than 292 px, which neither device is.

Screenshots: .denpa-work/shots/phone3.png, .denpa-work/shots/tablet.png.

Second defect in the same screen: no safe-area inset

On both devices the onboarding rail renders in the same band as the system status bar, so
待受 sits underneath the clock and status icons. The system status bar and the gesture pill
cannot be removed and are not the app's to reclaim — onboarding has to lay out around them.

Why it was not caught

#28 was built and verified against a browser at a resized window, where a 292 px card inside a
larger page is the intended desktop presentation. It was never run on a device. This is the same
shape as #33: the control looks right, the tests pass, and the thing does not work where it
actually runs.

Note that the rest of the chrome does not have this problem. receiver-chrome.tsx and
chrome-spec.ts carry no overall fixed size — only component-level ones (SIDE_KEY_WIDTH,
SCANLINE_HEIGHT), and the viewport dispatch in chrome-plan.ts drives the rest. Onboarding is
the outlier.

Acceptance

  • Onboarding fills the viewport on both target devices, in portrait and landscape.
  • The 292 px card presentation, if it is still wanted on desktop, is driven by the chrome plan
    the way every other band already is — not by a constant the renderer cannot override.
  • Nothing in onboarding renders underneath the system status bar or the gesture pill on either
    device.
  • Verified by running it on a device and capturing it, not by resizing a browser window.
  • #28 — onboarding, six steps
  • #46 — what one unified log looks like at phone width (the same viewport question, one screen over)
  • #48 — the ticket this was found under
Onboarding lays out as a fixed-size card, so on a real Android device it renders as a small box floating in a large black screen. Observed on both target devices, measured from screenshots rather than eyeballed. `src/renderer/src/components/receiver/onboarding-spec.ts:44-45`: ```ts export const SCREEN_WIDTH = '292px'; export const SCREEN_HEIGHT = '600px'; ``` applied at `src/renderer/src/components/receiver/onboarding.tsx:465-468`: ```tsx width={SCREEN_WIDTH} minHeight={SCREEN_HEIGHT} maxWidth="100%" margin="0 auto" ``` ## What it looks like | | Pixel 7 Pro | OnePlus Pad | |---|---|---| | Screen | 1440×3120 | 2000×2800 | | Density | 476 (DPR 2.975) | 400 (DPR 2.5) | | CSS viewport | ~484×1049 | 800×1120 | | Card, measured | 292×598 CSS px | 292 CSS px wide (731 device px) | | Share of width | ~60% | ~37% | The card is 292 px on both, so **the larger the screen the worse it reads** — on the tablet it occupies just over a third of the width, with the rest empty. `maxWidth="100%"` only helps on a viewport *narrower* than 292 px, which neither device is. Screenshots: `.denpa-work/shots/phone3.png`, `.denpa-work/shots/tablet.png`. ## Second defect in the same screen: no safe-area inset On both devices the onboarding rail renders in the same band as the system status bar, so `待受` sits underneath the clock and status icons. The system status bar and the gesture pill cannot be removed and are not the app's to reclaim — onboarding has to lay out around them. ## Why it was not caught `#28` was built and verified against a browser at a resized window, where a 292 px card inside a larger page is the intended desktop presentation. It was never run on a device. This is the same shape as `#33`: the control looks right, the tests pass, and the thing does not work where it actually runs. Note that **the rest of the chrome does not have this problem**. `receiver-chrome.tsx` and `chrome-spec.ts` carry no overall fixed size — only component-level ones (`SIDE_KEY_WIDTH`, `SCANLINE_HEIGHT`), and the viewport dispatch in `chrome-plan.ts` drives the rest. Onboarding is the outlier. ## Acceptance - Onboarding fills the viewport on both target devices, in portrait and landscape. - The 292 px card presentation, if it is still wanted on desktop, is driven by the chrome plan the way every other band already is — not by a constant the renderer cannot override. - Nothing in onboarding renders underneath the system status bar or the gesture pill on either device. - Verified by **running it on a device and capturing it**, not by resizing a browser window. ## Related - `#28` — onboarding, six steps - `#46` — what one unified log looks like at phone width (the same viewport question, one screen over) - `#48` — the ticket this was found under
Author
Owner

Fixed on main at 688fa2e (merge of 856ed03, branch issue/denpa-49-onboarding-fills-viewport).

What changed

The card. SCREEN_WIDTH/SCREEN_HEIGHT are deleted from onboarding-spec.ts. Onboarding's size is now a row in the same table every band reads: chrome-plan.ts gains an OnboardingPlan { contentMaxWidthPx, padding } on ChromePlan — mobile null, tablet-landscape 640 (ONBOARDING_MEASURE_PX). use-onboarding.ts measures the window with the existing useViewport and passes chromePlan(viewport).onboarding down, so the renderer stays a pure render and the window is still read in one place.

The root is position: fixed; inset: 0 on ground, so the ground is the screen at every size. Only the measure is capped, and only on the wide landscape screen — 640 of 1120 CSS px on the pad, over half the width, rather than a box in the middle of black. The content column scrolls inside the insets rather than the insets scrolling away with it.

The status bar. The cause was one line that was not there: src/renderer/index.html had no viewport meta at all. MainActivity.kt calls enableEdgeToEdge(), so the WebView is laid out over the status row and the gesture pill — and without viewport-fit=cover every env(safe-area-inset-*) reports 0. That silently disabled the insets receiver-chrome.tsx:289-292 already reserved, so the chrome had the same defect and nothing failed. The meta is added, and onboarding reserves the four insets the way the chrome's column does. App.tsx's approval overlay, which draws over onboarding, got max(16px, env(...)) for the same reason.

Verified on both devices

Run as the installed debug APK on each device and measured in the app's own WebView over CDP (webview_devtools_remote, Runtime.evaluate) — live DOM boxes, not a resized browser:

Pixel 7 Pro OnePlus Pad (portrait)
viewport 484 x 1048, DPR 2.975 800 x 1120, DPR 2.5
env(safe-area-inset-top/bottom) 49px / 24px 26px / 16px
onboarding root 484 x 1048 (was 292 x 598) 800 x 1120 (was 292 wide)
content column y 49, h 976 y 26, h 1078
step rail top (待受) 67 44

The insets reading non-zero is the proof the meta was the missing piece; the rail's top is below the status row on both, so 待受 no longer sits under the clock.

What I did not do

  • No pixel screenshot of the running app. Both devices are unattended and locked; screencap came back black on both, and logcat says why — the app's window has no surface while it is not visible (dispatchAppVisibility visible:false, isSurfaceValid:false, SkiaOpenGLPipeline::setSurface: surface=NULL). CDP Page.captureScreenshot hangs for the same reason (the target reports "visible": false). It is not caused by this change: the first screencaps, taken before the new APK was running, were black the same way. So "capture it" is met as measurement, not as an image, and eyeballing it on a woken device is still worth doing.
  • Landscape was not measured on-device. Forcing rotation over adb rotated the display (cur=2800x2000) but the invisible window was never re-laid out, so the WebView kept reporting 800 x 1120. Landscape is verified at the pad's landscape CSS size (1120 x 800) in a browser instead — root fills 1120 x 800, column 640 centred at x=240 on a full-bleed ground. The device half of that acceptance line is unverified.

Gates

npm test767 passed, 50 files; 6 new (three in onboarding.test.tsx for the full-viewport root, the reserved insets and the plan-driven measure; two in chrome-plan.test.ts; one guarding the deleted constants). cargo test18 passed. tsc -p tsconfig.web.json — 585 errors, the same 585 as main, none in the touched files.

Fixed on `main` at **688fa2e** (merge of `856ed03`, branch `issue/denpa-49-onboarding-fills-viewport`). ## What changed **The card.** `SCREEN_WIDTH`/`SCREEN_HEIGHT` are deleted from `onboarding-spec.ts`. Onboarding's size is now a row in the same table every band reads: `chrome-plan.ts` gains an `OnboardingPlan { contentMaxWidthPx, padding }` on `ChromePlan` — mobile `null`, tablet-landscape `640` (`ONBOARDING_MEASURE_PX`). `use-onboarding.ts` measures the window with the existing `useViewport` and passes `chromePlan(viewport).onboarding` down, so the renderer stays a pure render and the window is still read in one place. The root is `position: fixed; inset: 0` on `ground`, so the ground *is* the screen at every size. Only the measure is capped, and only on the wide landscape screen — 640 of 1120 CSS px on the pad, over half the width, rather than a box in the middle of black. The content column scrolls inside the insets rather than the insets scrolling away with it. **The status bar.** The cause was one line that was not there: `src/renderer/index.html` had **no viewport meta at all**. `MainActivity.kt` calls `enableEdgeToEdge()`, so the WebView is laid out over the status row and the gesture pill — and without `viewport-fit=cover` every `env(safe-area-inset-*)` reports `0`. That silently disabled the insets `receiver-chrome.tsx:289-292` already reserved, so the chrome had the same defect and nothing failed. The meta is added, and onboarding reserves the four insets the way the chrome's column does. `App.tsx`'s approval overlay, which draws over onboarding, got `max(16px, env(...))` for the same reason. ## Verified on both devices Run as the installed debug APK on each device and measured **in the app's own WebView over CDP** (`webview_devtools_remote`, `Runtime.evaluate`) — live DOM boxes, not a resized browser: | | Pixel 7 Pro | OnePlus Pad (portrait) | |---|---|---| | viewport | 484 x 1048, DPR 2.975 | 800 x 1120, DPR 2.5 | | `env(safe-area-inset-top/bottom)` | **49px / 24px** | **26px / 16px** | | onboarding root | **484 x 1048** (was 292 x 598) | **800 x 1120** (was 292 wide) | | content column | y 49, h 976 | y 26, h 1078 | | step rail top (待受) | **67** | **44** | The insets reading non-zero is the proof the meta was the missing piece; the rail's top is below the status row on both, so 待受 no longer sits under the clock. ## What I did not do - **No pixel screenshot of the running app.** Both devices are unattended and locked; `screencap` came back black on both, and logcat says why — the app's window has no surface while it is not visible (`dispatchAppVisibility visible:false`, `isSurfaceValid:false`, `SkiaOpenGLPipeline::setSurface: surface=NULL`). CDP `Page.captureScreenshot` hangs for the same reason (the target reports `"visible": false`). It is not caused by this change: the first screencaps, taken before the new APK was running, were black the same way. So "capture it" is met as **measurement**, not as an image, and eyeballing it on a woken device is still worth doing. - **Landscape was not measured on-device.** Forcing rotation over adb rotated the display (`cur=2800x2000`) but the invisible window was never re-laid out, so the WebView kept reporting 800 x 1120. Landscape is verified at the pad's landscape CSS size (1120 x 800) in a browser instead — root fills 1120 x 800, column 640 centred at x=240 on a full-bleed ground. The device half of that acceptance line is unverified. ## Gates `npm test` — **767 passed, 50 files**; 6 new (three in `onboarding.test.tsx` for the full-viewport root, the reserved insets and the plan-driven measure; two in `chrome-plan.test.ts`; one guarding the deleted constants). `cargo test` — **18 passed**. `tsc -p tsconfig.web.json` — 585 errors, the same 585 as `main`, none in the touched files.
aiko closed this issue 2026-07-30 18:49:43 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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
aiko/denpa#49
No description provided.