fix(horseshoe): extend mobile DM and Channels wrappers up over the safe-top zone so the StreamHeader curtain paints the status-bar strip on drag-up

This commit is contained in:
v.lagerev 2026-05-18 15:14:58 +03:00
parent 183351044a
commit 5e1944090e
2 changed files with 54 additions and 10 deletions

View file

@ -17,6 +17,18 @@ export const HORSESHOE_GAP_PX = VOJO_HORSESHOE_GAP_PX;
//
// `flex: 1` so the container fills whatever flex slot it's mounted in
// (PageNav's inner column for the Direct route).
//
// `marginTop: -var(--vojo-safe-top)` extends the container UP over the
// status-bar safe-top zone reserved by `PageNav` via `padding-top`. With
// this offset the wrapped StreamHeader's `curtain` (which positions
// `top: <negative>` when dragged past `closed`) can paint into the
// status-bar zone — without it, both `overflow: hidden` here and the
// `clipPath` on `appBody` would clip those pixels at the bottom of
// the status-bar strip and the strip would stay uncovered, breaking
// parity with Bots / ChannelsRoot (where StreamHeader is a direct
// child of PageNav and no such wrapper clip exists). The compensating
// `padding-top` lives on `appBody` so the wrapped DM list / tabs row
// stay visually anchored at the same Y as before the shift.
export const container = style({
position: 'relative',
display: 'flex',
@ -25,6 +37,7 @@ export const container = style({
minWidth: 0,
minHeight: 0,
overflow: 'hidden',
marginTop: 'calc(-1 * var(--vojo-safe-top, 0px))',
});
// === App body === Holds the wrapped children (the DM list — header,
@ -47,16 +60,25 @@ export const container = style({
// measured heights, and its top items in place; only the bottom edge
// of what's visible gets carved into the void below.
//
// `backgroundColor: Background.Container` is load-bearing: the
// container behind appBody is painted with the void colour (#090909)
// when the sheet is active, and without an opaque bg here the void
// would bleed through every transparent gap between DM list rows.
// The clip-path then carves away the bottom of this opaque pane,
// exposing the container's void colour only in the masked region —
// the only place we want the void to show.
// `backgroundColor: SurfaceVariant.Container` is load-bearing on two
// counts: (1) it must be OPAQUE so the container's void colour
// (painted inline when the sheet is active) doesn't bleed through gaps
// between DM list rows; (2) the safe-top padding region of THIS
// element is what paints the system-tray strip when the wrapper
// container is extended up over it (see `container.marginTop` above).
// Picking `SurfaceVariant.Container` (not `Background.Container`)
// matches the Bots / ChannelsRoot status-bar tone exactly — Bots
// renders `PageNav-inner.bg = SurfaceVariant.Container` in the safe-
// top zone, and the StreamHeader curtain (`Background.Container`)
// overpaints that lighter strip with the darker tone as it's dragged
// up. Mirroring the same two tones here gives Direct the same visible
// «curtain darkens the strip» transition the user expects.
//
// `flex: column` so the children (which expect a flex column parent
// — PageNav uses it) still stack naturally.
// — PageNav uses it) still stack naturally. `paddingTop:
// var(--vojo-safe-top)` reserves status-bar space INSIDE appBody so
// the wrapped StreamHeader.stage starts at the same Y as before the
// `container.marginTop` shift extended us upward.
export const appBody = style({
position: 'absolute',
top: 0,
@ -67,7 +89,8 @@ export const appBody = style({
flexDirection: 'column',
minWidth: 0,
minHeight: 0,
backgroundColor: color.Background.Container,
backgroundColor: color.SurfaceVariant.Container,
paddingTop: 'var(--vojo-safe-top, 0px)',
willChange: 'clip-path',
});

View file

@ -15,6 +15,16 @@ export const HORSESHOE_GAP_PX = VOJO_HORSESHOE_GAP_PX;
// the sheet is active. See MobileSettingsHorseshoe.css.ts for the full
// rationale on every property here — kept verbatim except for the file
// header.
//
// `marginTop: -var(--vojo-safe-top)` extends the container UP over the
// status-bar safe-top zone. Mirror of the same property in
// `MobileSettingsHorseshoe.css.ts::container` — without it, the wrapped
// StreamHeader's curtain (which positions `top: <negative>` when
// dragged past `closed`) is clipped by both `overflow: hidden` here
// and `appBody`'s clipPath at the bottom edge of the status bar, so
// the lighter `SurfaceVariant.Container` strip from `PageNav-inner`
// stays uncovered. The compensating `padding-top` lives on `appBody`
// so the wrapped channels list / tabs row stay visually anchored.
export const container = style({
position: 'relative',
display: 'flex',
@ -23,6 +33,7 @@ export const container = style({
minWidth: 0,
minHeight: 0,
overflow: 'hidden',
marginTop: 'calc(-1 * var(--vojo-safe-top, 0px))',
});
// Wrapped children (StreamHeader → ChannelsList → ChannelCreateRow →
@ -32,6 +43,15 @@ export const container = style({
// sheet is active, so without an opaque bg the void would bleed through
// every transparent gap between list rows. See canonical for the full
// reasoning on clip-path vs translate vs flex-shrink.
//
// `paddingTop: var(--vojo-safe-top)` reserves status-bar space INSIDE
// appBody — compensates the `container.marginTop: -safe-top` shift so
// the wrapped flex children (StreamHeader.stage) stay anchored at the
// same visual Y as before. `backgroundColor` is `SurfaceVariant.Container`
// (not `Background.Container`) so the now-visible safe-top zone of
// appBody matches the tone `PageNav-inner` shows in Bots / Channels-root
// (which use `surface="surfaceVariant"`), giving the curtain's darker
// `Background.Container` tone a visible strip to over-paint on drag-up.
export const appBody = style({
position: 'absolute',
top: 0,
@ -42,7 +62,8 @@ export const appBody = style({
flexDirection: 'column',
minWidth: 0,
minHeight: 0,
backgroundColor: color.Background.Container,
backgroundColor: color.SurfaceVariant.Container,
paddingTop: 'var(--vojo-safe-top, 0px)',
willChange: 'clip-path',
});