feat(stream-header): flatten web curtain to a tabsRow-border divider pixel-aligned with PageHeader at WEB_TABS_ROW_PX=54 and gate keyboard probe to native
This commit is contained in:
parent
cde50cff0f
commit
78504262d3
3 changed files with 89 additions and 10 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
import { color, toRem } from 'folds';
|
||||
import { color, config, toRem } from 'folds';
|
||||
import {
|
||||
CHIP_GAP_PX,
|
||||
CURTAIN_BREATHER_PX,
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
CURTAIN_SNAP_MS,
|
||||
HANDLE_HEIGHT_PX,
|
||||
TABS_ROW_PX,
|
||||
WEB_TABS_ROW_PX,
|
||||
} from './geometry';
|
||||
|
||||
// Stage. Position-relative anchor. The header itself paints the
|
||||
|
|
@ -63,13 +64,31 @@ export const header = style({
|
|||
});
|
||||
|
||||
// Tabs row. Stays fully visible regardless of curtain position
|
||||
// because the curtain's `top` floor equals `TABS_ROW_PX`.
|
||||
// because the curtain's `top` floor equals `TABS_ROW_PX` on native
|
||||
// (`WEB_TABS_ROW_PX` on web — see `geometry.ts::WEB_TABS_ROW_PX`).
|
||||
//
|
||||
// Web variant: shrink to `WEB_TABS_ROW_PX` (= 54 px = folds Header
|
||||
// `size="600"`) so the row reads at the same height as the right-pane
|
||||
// room `PageHeader`, AND own the 1 px divider rule as a
|
||||
// `border-bottom`. Putting the rule on `tabsRow` (not on the curtain
|
||||
// as a `border-top`) is load-bearing for pixel alignment: with the
|
||||
// global `* { box-sizing: border-box }` reset (`src/index.css`),
|
||||
// `tabsRow`'s 1 px bottom border lands at y=53→54 inside the 54 px
|
||||
// box — exactly where PageHeader's outlined border-bottom paints. If
|
||||
// the rule lived on the curtain's `border-top` at `top: 54`, it would
|
||||
// paint at y=54→55, off-by-one against the right pane.
|
||||
export const tabsRow = style({
|
||||
flexShrink: 0,
|
||||
height: toRem(TABS_ROW_PX),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: `0 ${toRem(8)}`,
|
||||
selectors: {
|
||||
'[data-platform="web"] &': {
|
||||
height: toRem(WEB_TABS_ROW_PX),
|
||||
borderBottom: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const tabsCluster = style({
|
||||
|
|
@ -92,16 +111,29 @@ export const iconsCluster = style({
|
|||
// child (DirectSelfRow / WorkspaceFooter) stays glued to the visible
|
||||
// viewport bottom regardless of where the curtain's top is.
|
||||
//
|
||||
// Only the TOP corners are rounded: the bottom is meant to read as
|
||||
// continuous with the always-visible bottomPinned row (DirectSelfRow
|
||||
// is the curtain's last flex child) — adding `borderBottomRadius`
|
||||
// would crop the row's corners against the curtain's
|
||||
// `overflow: hidden`, which visually reads as «a light-blue strip
|
||||
// cuts into the row».
|
||||
// On native, only the TOP corners are rounded: the bottom is meant
|
||||
// to read as continuous with the always-visible bottomPinned row
|
||||
// (DirectSelfRow is the curtain's last flex child) — adding
|
||||
// `borderBottomRadius` would crop the row's corners against the
|
||||
// curtain's `overflow: hidden`, which visually reads as «a light-
|
||||
// blue strip cuts into the row».
|
||||
//
|
||||
// Live finger tracking and snap commits both flow through React state
|
||||
// updates to `top` so the transition is always coordinated with the
|
||||
// rendered position — disabled during drag, restored on commit.
|
||||
//
|
||||
// Web variant (`[data-platform="web"]` on `stage`, set by
|
||||
// StreamHeader.tsx when `!isNativePlatform()`): there is no pin/peek
|
||||
// gesture, so the curtain is a purely static slab under the tabs row.
|
||||
// Drop ONLY the «card» rounding (top corners flat). The divider rule
|
||||
// at the seam is owned by `tabsRow.borderBottom` under the same
|
||||
// selector — that placement keeps the rule pixel-aligned with the
|
||||
// right-pane `PageHeader`'s outlined border (see `tabsRow` comment
|
||||
// above). The curtain bg stays `Background.Container` so the chat-
|
||||
// row rows (`NavItem variant="Background"`) keep blending into one
|
||||
// continuous list surface — if we made the curtain transparent the
|
||||
// rows would paint as dark cards over the lighter
|
||||
// `SurfaceVariant.Container` stage.
|
||||
export const curtain = style({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
|
|
@ -119,6 +151,12 @@ export const curtain = style({
|
|||
// Hint the compositor while the curtain is moving. Cheap since the
|
||||
// curtain is the only element in this stacking context that animates.
|
||||
willChange: 'top',
|
||||
selectors: {
|
||||
'[data-platform="web"] &': {
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Drag handle at the top of the curtain. Dedicated touch surface for
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
StreamHeaderPrimaryAction,
|
||||
mobilePagerCurtainAtom,
|
||||
} from '../../state/mobilePagerHeader';
|
||||
import { TABS_ROW_PX, WEB_TABS_ROW_PX } from './geometry';
|
||||
import { settingsSheetAtom } from '../../state/settingsSheet';
|
||||
import { channelsWorkspaceSheetAtom } from '../../state/channelsWorkspaceSheet';
|
||||
import * as css from './StreamHeader.css';
|
||||
|
|
@ -247,9 +248,20 @@ export function StreamHeader({
|
|||
// stage (= y = safe-top in viewport), covering the tabs row. The
|
||||
// global pinned atom shares this state across every listing tab so
|
||||
// swiping between Direct / Channels / Bots preserves the lock.
|
||||
//
|
||||
// `platformOffset` is the web-only shift that lifts every non-pinned
|
||||
// snap by the delta between native and web tabs-row heights. Tabs
|
||||
// row on web is `WEB_TABS_ROW_PX` (= 54px, matching PageHeader on
|
||||
// the right pane); `snapTopPx` is computed against `TABS_ROW_PX`
|
||||
// (= 64px) which stays authoritative for native pin/peek geometry.
|
||||
// Subtracting the delta on web realigns the closed/form snaps with
|
||||
// the smaller tabs row without touching the snap-state machine.
|
||||
// Pinned (= 0) doesn't need the offset because the safe-top + native
|
||||
// contract owns that case and pinned is native-only.
|
||||
const platformOffset = isNativePlatform() ? 0 : WEB_TABS_ROW_PX - TABS_ROW_PX;
|
||||
const curtainTop = curtain.pinned
|
||||
? 0 + curtain.liveDragPx
|
||||
: snapTopPx(curtain.snap, curtain.formHeightPx) + curtain.liveDragPx;
|
||||
: snapTopPx(curtain.snap, curtain.formHeightPx) + platformOffset + curtain.liveDragPx;
|
||||
|
||||
// After the curtain settles at `closed`, unmount any lingering form.
|
||||
// Guarded so unrelated transitionend events (e.g. children's own
|
||||
|
|
@ -286,6 +298,15 @@ export function StreamHeader({
|
|||
// spurious flips on small browser-chrome animations.
|
||||
const [keyboardOpen, setKeyboardOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
// Desktop browsers / Electron have no soft keyboard, but their
|
||||
// VisualViewport DOES shrink on aggressive page zoom (Ctrl+`+`).
|
||||
// That used to slip past as a hidden quirk while the curtain
|
||||
// rendered as a rounded card; once the web variant flattened the
|
||||
// curtain it became a visible regression — the DirectSelfRow at
|
||||
// the bottom would collapse to height: 0 under zoom and read as
|
||||
// broken layout. Gate the listener to native so the probe only
|
||||
// arms where a real soft keyboard can actually appear.
|
||||
if (!isNativePlatform()) return undefined;
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return undefined;
|
||||
const KEYBOARD_PROBE_PX = 100;
|
||||
|
|
@ -308,7 +329,7 @@ export function StreamHeader({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className={css.stage}>
|
||||
<div className={css.stage} data-platform={isNativePlatform() ? undefined : 'web'}>
|
||||
<header className={css.header}>
|
||||
{/* ── Tabs row + action icons (always visible) ───────────
|
||||
In pager mode the row stays mounted (curtain snap math
|
||||
|
|
|
|||
|
|
@ -34,6 +34,26 @@
|
|||
// Tabs row height. Always visible above the curtain.
|
||||
export const TABS_ROW_PX = 64;
|
||||
|
||||
// Web-only tabs-row height override. Matches folds `<Header size="600">`
|
||||
// = 3.375rem = 54 px, which is the height the right-side `PageHeader`
|
||||
// in the room chat panel (see `components/page/Page.tsx::PageHeader`)
|
||||
// renders at. The 1 px divider rule on web lives as `tabsRow.
|
||||
// borderBottom` so — under the global `* { box-sizing: border-box }`
|
||||
// reset — it lands at y=53→54 inside this 54 px box, exactly matching
|
||||
// where PageHeader's `outlined: true` border-bottom paints on the
|
||||
// right pane. The two panes thus share one visible header baseline.
|
||||
//
|
||||
// Native keeps `TABS_ROW_PX` because the pin-gesture travel
|
||||
// (`PIN_TRAVEL_PX`) is anchored to it and shrinking it would change
|
||||
// the curtain's snap geometry. Web has no pin gesture, so the override
|
||||
// is applied through two coordinated levers:
|
||||
// 1. CSS `[data-platform="web"] &` selectors on `tabsRow` (height
|
||||
// → `WEB_TABS_ROW_PX`, plus the divider as `borderBottom`).
|
||||
// 2. TSX `platformOffset = WEB_TABS_ROW_PX - TABS_ROW_PX` (= -10)
|
||||
// added to `curtainTop` so the closed / peek / form snaps all
|
||||
// ride the reduced tabs row without recomputing `snapTopPx`.
|
||||
export const WEB_TABS_ROW_PX = 54;
|
||||
|
||||
// Each peek-chip row. Reveals one chip's pill (h=48) + 8px top breather.
|
||||
export const CHIP_ROW_PX = 56;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue