feat(safe-area): extend Android edge-to-edge top inset via --vojo-safe-top var and collapse profile horseshoe header with measured height
This commit is contained in:
parent
ce82d66883
commit
149382299a
10 changed files with 232 additions and 76 deletions
|
|
@ -20,7 +20,17 @@ export function Modal500({ requestClose, children }: Modal500Props) {
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Modal size="500" variant="Background">
|
||||
<Modal
|
||||
size="500"
|
||||
variant="Background"
|
||||
// Reset `--vojo-safe-top` for everything mounted inside the
|
||||
// dialog. The Android status-bar inset is reserved by each
|
||||
// page header's `padding-top: var(--vojo-safe-top)` for
|
||||
// top-of-screen surfaces — but a centred 500px modal sits
|
||||
// away from the screen edge, and the same padding inside it
|
||||
// just adds dead space above its header.
|
||||
style={{ ['--vojo-safe-top' as string]: '0px' }}
|
||||
>
|
||||
{/* PageRoot rendered inside the dialog (Settings,
|
||||
SpaceSettings, RoomSettings) would otherwise pick up
|
||||
the web horseshoe layout — void column + rounded
|
||||
|
|
|
|||
|
|
@ -141,6 +141,15 @@ export function PageNav({
|
|||
grow="Yes"
|
||||
direction="Column"
|
||||
className={horseshoe ? css.PageNavInnerWebHorseshoe : undefined}
|
||||
// Top inset for native: `#root` no longer reserves the status-bar
|
||||
// height (src/index.css), so the page-nav extends to the screen
|
||||
// top. The padding here pushes the page-nav header (workspace
|
||||
// tabs, etc.) below the status-bar icons. Applied at the inner
|
||||
// column rather than at the `PageNavHeader` recipe because the
|
||||
// recipe uses a Folds `<Header size="...">` with a fixed height —
|
||||
// padding there would clip the header content. `--vojo-safe-top`
|
||||
// is 0 on web and inside Modal500-hosted dialogs.
|
||||
style={{ paddingTop: 'var(--vojo-safe-top, 0px)' }}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
|
|
@ -268,6 +277,10 @@ function ResizablePageNav({ children }: { children: ReactNode }) {
|
|||
grow="Yes"
|
||||
direction="Column"
|
||||
className={horseshoe ? css.PageNavInnerWebHorseshoe : undefined}
|
||||
// Same native safe-top inset as the regular PageNav above —
|
||||
// `var(--vojo-safe-top)` is 0 on web (where resizable is used)
|
||||
// but kept here for symmetry / future-proofing.
|
||||
style={{ paddingTop: 'var(--vojo-safe-top, 0px)' }}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,20 @@ export const Shell = style([
|
|||
flexDirection: 'column',
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
overflow: 'hidden',
|
||||
// Native safe-top: `#root` no longer reserves the status-bar inset
|
||||
// (src/index.css), so BotShell extends to the screen top. The
|
||||
// padding keeps the Hero (avatar + title + actions) clear of the
|
||||
// system icons. Shell bg already matches the widget body tone, so
|
||||
// the padding zone reads as a continuation of the bot surface. On
|
||||
// web `--vojo-safe-top` is 0.
|
||||
//
|
||||
// Bottom inset is intentionally NOT added here: the iframe inside
|
||||
// `Frame` paints its own body bg (#181a20, see widget-telegram
|
||||
// styles.css) and the widget is responsible for the gesture-pill
|
||||
// clearance of its own action rows. Padding Shell here exposed a
|
||||
// visible seam between the iframe area and the env-bottom-tall
|
||||
// strip below it on Android.
|
||||
paddingTop: 'var(--vojo-safe-top, 0px)',
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Box, Line, toRem } from 'folds';
|
||||
import { useMatch, useParams } from 'react-router-dom';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
|
|
@ -112,25 +112,6 @@ export function Room({ renderRoomView }: RoomProps) {
|
|||
const profileOpen = !!useAtomValue(userRoomProfileAtom);
|
||||
const showProfileHorseshoe = profileOpen && !isMobile && !showThreadDrawer;
|
||||
|
||||
// Match the Android edge-to-edge safe-area zones (status bar + gesture
|
||||
// pill) to the chat surface while a Room is mounted: `body` reads
|
||||
// `--vojo-safe-area-bg`, which `app/styles/global.css.ts` defaults to
|
||||
// `#0d0e11` (chat-list tone). We retune to `#181a20`
|
||||
// (`SurfaceVariant.Container`, the chat tone painted by RoomView's
|
||||
// `<Page>`) for the lifetime of Room and `removeProperty` on cleanup
|
||||
// so the chat-list view goes back to its dark tone. Hardcoded because
|
||||
// folds tokens are scoped to the `.dark-theme` class on `body` —
|
||||
// writing `var(--xxx)` on `:root` (where `--vojo-safe-area-bg` lives)
|
||||
// would leave it unresolved and body would fall back to the literal
|
||||
// `#0d0e11` in index.css.
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--vojo-safe-area-bg', '#181a20');
|
||||
return () => {
|
||||
root.style.removeProperty('--vojo-safe-area-bg');
|
||||
};
|
||||
}, []);
|
||||
|
||||
useKeyDown(
|
||||
window,
|
||||
useCallback(
|
||||
|
|
@ -173,8 +154,13 @@ export function Room({ renderRoomView }: RoomProps) {
|
|||
grow="Yes"
|
||||
direction="Column"
|
||||
className={
|
||||
showProfileHorseshoe ? ContainerColor({ variant: 'Background' }) : undefined
|
||||
showProfileHorseshoe
|
||||
? ContainerColor({ variant: 'Background' })
|
||||
: undefined
|
||||
}
|
||||
// No chat-column padding-top / bg: the silhouette inside
|
||||
// `MobileProfileHorseshoe` owns the safe-top inset and bg.
|
||||
// See the !callView twin block below for the rationale.
|
||||
>
|
||||
<RoomViewProfilePanel header={<RoomViewHeader callView />}>
|
||||
<Box grow="Yes">
|
||||
|
|
@ -188,8 +174,18 @@ export function Room({ renderRoomView }: RoomProps) {
|
|||
grow="Yes"
|
||||
direction="Column"
|
||||
className={
|
||||
showProfileHorseshoe ? ContainerColor({ variant: 'Background' }) : undefined
|
||||
showProfileHorseshoe
|
||||
? ContainerColor({ variant: 'Background' })
|
||||
: undefined
|
||||
}
|
||||
// No padding-top / bg on mobile chat column: the silhouette
|
||||
// inside `MobileProfileHorseshoe` permanently sits at body_top
|
||||
// with its own `padding-top: var(--vojo-safe-top)` keeping
|
||||
// chat-header / panel content below the status-bar icons.
|
||||
// The silhouette's bg fades between chat-surface tone (when
|
||||
// closed) and user-card tone (when fully open) as the user
|
||||
// drags. Desktop branch still works because `--vojo-safe-top`
|
||||
// resolves to 0 on web.
|
||||
>
|
||||
<RoomViewProfilePanel header={<RoomViewHeader />}>
|
||||
<Box grow="Yes">
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ export const HORSESHOE_GAP_PX = 12;
|
|||
// Outer container — flex column hosting the silhouette (panel +
|
||||
// header live inside) and the chat body below. `overflow: hidden`
|
||||
// clips the chat body's rounded top corners against the container's
|
||||
// own edges; `background` paints the dark «void» behind the gap
|
||||
// between silhouette and chat body when the drawer is open.
|
||||
// own edges when the user-card rail is open and the chatBody margin-
|
||||
// top void gap opens up; without this clip the rounded-corner cut
|
||||
// areas would reveal whatever's behind the chat column instead of
|
||||
// the horseshoe void colour painted by `containerStyle`.
|
||||
export const container = style({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
|
|
@ -70,11 +72,26 @@ export const panelViewport = style({
|
|||
userSelect: 'none',
|
||||
});
|
||||
|
||||
// Anchor at the TOP of `panelViewport` so the card slides downward as
|
||||
// `panelViewport.height` grows: hero avatar visible first, info rows
|
||||
// behind it, drag handle last. Previously `bottom: 0` made the handle
|
||||
// reveal first and the hero last — which read as "dark rising from the
|
||||
// chat header" once the silhouette also extended through the status-bar
|
||||
// zone above. Trade-off: drag handle is hidden mid-drag, only visible
|
||||
// when fully open; the panel surface stays drag-sensitive throughout.
|
||||
//
|
||||
// `padding-top: var(--vojo-safe-top)` keeps the user-card content (hero
|
||||
// avatar + info rows) clear of the Android status-bar icons in the
|
||||
// open state. The padding zone shows the silhouette's bg
|
||||
// (`Background.Container`, #0d0e11) through panelContent's transparent
|
||||
// background, so the status-bar zone reads as part of the dark user
|
||||
// card when the panel is open.
|
||||
export const panelContent = style({
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
paddingTop: 'var(--vojo-safe-top, 0px)',
|
||||
});
|
||||
|
||||
export const panelInner = style({
|
||||
|
|
@ -160,22 +177,46 @@ export const avatarFullFallback = style({
|
|||
});
|
||||
|
||||
// === Header viewport === Holds the chat header as the silhouette's
|
||||
// bottom child. Same CSS-grid `1fr → 0fr` row-track trick the legacy
|
||||
// `headerWrap` used so the row animates smoothly without measuring
|
||||
// the header's intrinsic height in JS. The inner div clips the
|
||||
// header content as the track collapses. `userSelect: none` keeps a
|
||||
// mouse drag-down-to-open gesture from racing native text-selection
|
||||
// on the chat title — same rationale as on `panelViewport`.
|
||||
// bottom child. Animated via an explicit `height` set inline (driven
|
||||
// by `(1 - expandedFraction) * headerNaturalHeight`, where
|
||||
// `headerNaturalHeight` is measured via `useLayoutEffect` +
|
||||
// `ResizeObserver` on the inner block). The legacy
|
||||
// `grid-template-rows: 1fr → 0fr` trick was abandoned because Folds
|
||||
// `<Header size="600">` enforces a `min-height` that prevented the
|
||||
// grid track from collapsing to 0 — leaving a light-blue strip at
|
||||
// the bottom of the user card. `overflow: hidden` keeps the chat
|
||||
// header content clipped while the viewport height interpolates.
|
||||
// `userSelect: none` keeps a mouse drag-down-to-open gesture from
|
||||
// racing native text-selection on the chat title — same rationale
|
||||
// as on `panelViewport`.
|
||||
export const headerViewport = style({
|
||||
display: 'grid',
|
||||
flexShrink: 0,
|
||||
willChange: 'grid-template-rows',
|
||||
overflow: 'hidden',
|
||||
willChange: 'height',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
// `padding-top: var(--vojo-safe-top)` pushes the chat header's content
|
||||
// (back arrow, avatar, name) below the Android status-bar icons in
|
||||
// edge-to-edge mode — `#root` no longer reserves the inset itself
|
||||
// (src/index.css), so the chat header would otherwise land directly
|
||||
// under the system icons. The padding zone is painted with
|
||||
// `SurfaceVariant.Container` (the chat-header bg below it) so the
|
||||
// visible strip against the status-bar icons reads as one continuous
|
||||
// header band — no seam at the inset boundary.
|
||||
//
|
||||
// `box-sizing: border-box` is load-bearing for the height animation
|
||||
// that `headerViewport` runs (see comment there): when the outer height
|
||||
// reaches 0 px on full rail-open, the `padding-top` is included in the
|
||||
// 0-tall box and the SurfaceVariant strip collapses with it. With
|
||||
// `content-box` it would persist as a `var(--vojo-safe-top)`-tall band
|
||||
// at the bottom of the user card.
|
||||
export const headerViewportInner = style({
|
||||
boxSizing: 'border-box',
|
||||
minHeight: 0,
|
||||
overflow: 'hidden',
|
||||
paddingTop: 'var(--vojo-safe-top, 0px)',
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
});
|
||||
|
||||
// === Chat body === Timeline + input below the silhouette. The
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
// `RoomViewProfileSidePanel`, which `Room.tsx` mounts as a sibling
|
||||
// of the chat column.
|
||||
|
||||
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import React, { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { config } from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
|
|
@ -38,6 +38,7 @@ import { getMxIdLocalPart, guessDmRoomUserId, mxcUrlToHttp } from '../../utils/m
|
|||
import { UserRoomProfile } from '../../components/user-profile/UserRoomProfile';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { VOJO_HORSESHOE_VOID_COLOR } from '../../styles/horseshoe';
|
||||
import * as css from './RoomViewProfilePanel.css';
|
||||
|
||||
// Card height as a fraction of the viewport.
|
||||
|
|
@ -128,6 +129,19 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
|||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const headerInnerRef = useRef<HTMLDivElement>(null);
|
||||
// Measured natural height of the chat-header inner block (incl. its
|
||||
// `padding-top: var(--vojo-safe-top)`). Drives the explicit `height`
|
||||
// animation on `headerViewport` — replaces the legacy `grid-template-
|
||||
// rows: 1fr → 0fr` trick because Folds `<Header size="600">` enforces
|
||||
// a `min-height` that prevents the grid track from collapsing fully
|
||||
// to 0, leaving a light-blue strip at the bottom of the user card
|
||||
// when the rail is fully open. Measured via `useLayoutEffect` so the
|
||||
// first paint already uses the right value (no flicker on cold
|
||||
// mount) and re-measured by ResizeObserver whenever the chat-header
|
||||
// content reflows (e.g. online tag appears, env-inset changes on
|
||||
// rotation).
|
||||
const [headerNaturalHeight, setHeaderNaturalHeight] = useState(0);
|
||||
|
||||
// Close profile when the room changes — atom is global state and
|
||||
// would otherwise carry the previous room's userId into this room.
|
||||
|
|
@ -150,6 +164,25 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
|||
return () => window.removeEventListener('resize', onResize);
|
||||
}, []);
|
||||
|
||||
// Measure the chat header's natural height (incl. its safe-top padding).
|
||||
// `scrollHeight` returns the content size even while the outer is
|
||||
// constrained by our animated `height` — so we keep getting the right
|
||||
// natural value across reflows. `useLayoutEffect` does the first
|
||||
// measurement synchronously after DOM mutation, before paint, so the
|
||||
// first frame already uses the measured value.
|
||||
useLayoutEffect(() => {
|
||||
const el = headerInnerRef.current;
|
||||
if (!el) return undefined;
|
||||
const measure = () => {
|
||||
const next = el.scrollHeight;
|
||||
if (next > 0) setHeaderNaturalHeight(next);
|
||||
};
|
||||
measure();
|
||||
const ro = new ResizeObserver(measure);
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
const open = !!profileState;
|
||||
const baseExpanded = open ? railHeightPx : 0;
|
||||
const expandedPx = drag
|
||||
|
|
@ -372,7 +405,7 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
|||
: `height ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
||||
const headerViewportTransition = isDragging
|
||||
? 'none'
|
||||
: `grid-template-rows ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
||||
: `height ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
||||
const silhouetteTransition = isDragging
|
||||
? 'none'
|
||||
: `border-bottom-left-radius ${ANIMATION_MS}ms ${VAUL_EASING}, border-bottom-right-radius ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
||||
|
|
@ -381,7 +414,7 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
|||
: `margin-top ${ANIMATION_MS}ms ${VAUL_EASING}, border-top-left-radius ${ANIMATION_MS}ms ${VAUL_EASING}, border-top-right-radius ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
backgroundColor: horseshoeActive ? '#090909' : undefined,
|
||||
backgroundColor: horseshoeActive ? VOJO_HORSESHOE_VOID_COLOR : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -482,15 +515,22 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
|||
ref={headerRef}
|
||||
className={css.headerViewport}
|
||||
style={{
|
||||
// Collapse the chat header to 0 height as the panel
|
||||
// expands — same `1fr → 0fr` row-track trick the legacy
|
||||
// layout used. CSS-grid track interpolation is animatable
|
||||
// (unlike `height: auto`) and avoids measuring the
|
||||
// header's intrinsic height in JS. As the row collapses,
|
||||
// the silhouette's bottom edge moves from header-bottom
|
||||
// up to panel-bottom; the rounded corners stay anchored
|
||||
// there because they belong to the silhouette wrapper.
|
||||
gridTemplateRows: `${1 - expandedFraction}fr`,
|
||||
// Animate the chat header's height from its measured natural
|
||||
// size down to 0 as the user-card panel expands. Linear scale
|
||||
// by `(1 - expandedFraction)` so the panel and the header
|
||||
// grow / shrink in lockstep. Falls back to `auto` until the
|
||||
// first measurement lands (cold mount before useLayoutEffect
|
||||
// runs) so the closed-state header isn't briefly squashed to
|
||||
// 0. Replaces the legacy `grid-template-rows: 1fr → 0fr`
|
||||
// trick because Folds `<Header size="600">` enforces a
|
||||
// `min-height` that prevented the grid track from collapsing
|
||||
// to 0, leaving a light-blue strip at the bottom of the
|
||||
// user card. With explicit `height` + `overflow: hidden` on
|
||||
// the viewport (see css), `height: 0` is forced honest.
|
||||
height:
|
||||
headerNaturalHeight > 0
|
||||
? `${(1 - expandedFraction) * headerNaturalHeight}px`
|
||||
: 'auto',
|
||||
transition: headerViewportTransition,
|
||||
// While the rail is mostly open the (collapsed) header
|
||||
// shouldn't intercept drag-back-to-open gestures —
|
||||
|
|
@ -499,7 +539,9 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
|||
touchAction: headerDragEnabled ? 'pan-x' : undefined,
|
||||
}}
|
||||
>
|
||||
<div className={css.headerViewportInner}>{header}</div>
|
||||
<div ref={headerInnerRef} className={css.headerViewportInner}>
|
||||
{header}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@ export const ThreadDrawer = style({
|
|||
});
|
||||
|
||||
// Mobile push: drawer occupies the whole content column, not a side pane.
|
||||
// `padding-top: var(--vojo-safe-top)` keeps the drawer's `ThreadDrawerHeader`
|
||||
// (back arrow + thread title + counter) clear of the Android status-bar
|
||||
// icons. `#root` no longer reserves the inset itself (`src/index.css`),
|
||||
// and the chat-column wrapper that hosts this drawer on mobile has no
|
||||
// safe-top of its own (the silhouette in `RoomViewProfilePanel` owns the
|
||||
// inset for the chat header — but the thread drawer mounts as a sibling
|
||||
// and would otherwise land directly under the system icons). The desktop
|
||||
// `ThreadDrawer` style sits inside the chat row that has `env()` left/right
|
||||
// only, so it inherits no top inset and doesn't need this padding (and
|
||||
// would just leave dead space below the chat-column-wrapper's chrome).
|
||||
export const ThreadDrawerMobile = style({
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
|
|
@ -27,6 +37,7 @@ export const ThreadDrawerMobile = style({
|
|||
flexDirection: 'column',
|
||||
backgroundColor: color.Surface.Container,
|
||||
minHeight: 0,
|
||||
paddingTop: 'var(--vojo-safe-top, 0px)',
|
||||
});
|
||||
|
||||
export const ThreadDrawerHeader = style({
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ export const AuthLayout = style({
|
|||
// chat surface so cold start / auth read as one continuous canvas.
|
||||
background: '#0d0e11',
|
||||
color: '#e8e4df',
|
||||
// Status-bar inset for native: `#root` no longer reserves it
|
||||
// (src/index.css), so the AuthLayout backdrop now extends through the
|
||||
// status-bar zone. Padding pushes the mascot / modal stack below the
|
||||
// system icons. AuthLayout reads `#root`'s computed paddingTop into
|
||||
// `padTop` (AuthLayout.tsx) for its anchor math; with that 0 the math
|
||||
// sees the full body height and positions content against the local
|
||||
// padding instead.
|
||||
paddingTop: 'var(--vojo-safe-top, 0px)',
|
||||
boxSizing: 'border-box',
|
||||
|
||||
'@media': {
|
||||
'only screen and (max-width: 768px)': {
|
||||
|
|
|
|||
|
|
@ -1,27 +1,49 @@
|
|||
import { globalStyle } from '@vanilla-extract/css';
|
||||
import { color } from 'folds';
|
||||
|
||||
// Vojo-owned safe-area background. Used by `body { background-color: ... }`
|
||||
// in `src/index.css` to paint Android edge-to-edge cutout / nav-bar zones.
|
||||
// Matches `Background.Container` from the Dawn palette in `src/colors.css.ts`
|
||||
// (`#0d0e11`, canon DAWN.bg2). The safe-area zone reads as a continuation of
|
||||
// the sidebar / nav panels — no visible color seam at the system-bar boundary.
|
||||
// Android edge-to-edge top inset exposed as a plain CSS var so the
|
||||
// top-anchored UIs that need it (PageNav inner column,
|
||||
// `RoomViewProfilePanel.headerViewportInner` / `.panelContent`,
|
||||
// `BotShell.Shell`, `AuthLayout`, `ThreadDrawerMobile`) can pad
|
||||
// themselves down by exactly the status-bar height without
|
||||
// re-evaluating `env(...)` at each call site. `#root` no longer
|
||||
// reserves this inset itself (`src/index.css`); the chat surface /
|
||||
// chat-list shell extend their own bg through the status-bar zone and
|
||||
// push visible content below the system icons via this var. `Modal500`
|
||||
// resets the var to `0px` inside the dialog subtree so internal
|
||||
// headers don't double-pad. On web `env()` resolves to `0px`, so the
|
||||
// var is a no-op there.
|
||||
//
|
||||
// Why a custom var (and not `color.Background.Container`):
|
||||
// 1. Folds emits its color tokens scoped to a theme class (e.g. `--oq6d070`
|
||||
// lives inside the `.lightTheme` selector). Before useTheme appends a
|
||||
// theme class to body, `var(--oq6d070)` resolves to its initial value
|
||||
// and `background-color` falls back to transparent — system bars show
|
||||
// through unpainted on Android edge-to-edge. A `:root`-level var here
|
||||
// resolves from the very first paint.
|
||||
// 2. `--oq6d070` is a folds@2.6 internal vanilla-extract debugId hash and
|
||||
// will rotate on any folds upgrade.
|
||||
//
|
||||
// The hardcoded #0d0e11 in `index.css` is the matching fallback for the
|
||||
// instant before this CSS file is parsed.
|
||||
//
|
||||
// See docs/plans/dm_1x1_redesign.md §6.6 / R13.
|
||||
// Bottom inset is owned per-component: surfaces that anchor
|
||||
// interactive content at the screen bottom and need 3-button-nav /
|
||||
// home-indicator clearance read `env(safe-area-inset-bottom)`
|
||||
// themselves (e.g. `SyncIndicator.css.ts`). The chat surface /
|
||||
// composer extend flush to `body_bottom` by design.
|
||||
globalStyle(':root', {
|
||||
vars: {
|
||||
'--vojo-safe-area-bg': '#0d0e11',
|
||||
'--vojo-safe-top': 'env(safe-area-inset-top, 0px)',
|
||||
},
|
||||
});
|
||||
|
||||
// Vojo-owned safe-area background. Used by `body { background-color: ... }`
|
||||
// in `src/index.css` to paint the body region behind any `env()` cut-out
|
||||
// zone (display notch, left/right inset) — defended in depth even though
|
||||
// `#root` covers most of the layout edge-to-edge now.
|
||||
//
|
||||
// Bound to the folds `Background.Container` token (Dawn `#0d0e11` in
|
||||
// dark; will reshade automatically when a light theme is added). The
|
||||
// var lives on `body` rather than `:root` because folds attaches the
|
||||
// theme class (`.dark-theme` / `.lightTheme`) to `document.body` —
|
||||
// `var(--folds-token)` only resolves at scopes the theme class
|
||||
// dominates. A `:root` host would freeze the var at the folds token's
|
||||
// initial value (`transparent` in dark / `#fff` in light) and the
|
||||
// safe-area would flash unpainted before `ThemeManager` mounts. Hosting
|
||||
// on body keeps the var resolution in lockstep with whatever theme
|
||||
// `ThemeManager` has applied; the literal `#0d0e11` fallback in
|
||||
// `index.css`'s `body { background-color }` declaration covers the
|
||||
// cold-start window before this stylesheet parses.
|
||||
globalStyle('body', {
|
||||
vars: {
|
||||
'--vojo-safe-area-bg': color.Background.Container,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -60,15 +60,13 @@ body {
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Top / left / right insets keep app chrome clear of system bars and
|
||||
display cutouts. Bottom inset is intentionally zero so the chat
|
||||
surface (RoomView's `<Page>`) and the chat-list shell (PageRoot's
|
||||
Background-coloured outer Box) extend all the way to the screen
|
||||
edge — same edge-to-edge pattern WhatsApp / Telegram use. Without
|
||||
this, the bottom env zone showed a different colour from the
|
||||
painted surface, and the composer's wrap-padding-bottom kept
|
||||
overlapping the gesture-pill area, visually "eating" the form. */
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
|
||||
/* Top & bottom insets are intentionally zero: each screen extends
|
||||
its own bg through the system-bar zones (status bar at top, gesture
|
||||
pill at bottom). Top-anchored UI pads itself down by `--vojo-safe-top`
|
||||
(see `app/styles/global.css.ts`) — covered: PageHeader, PageNavHeader
|
||||
(chat / chat-list headers via recipe), AuthLayout root. Side insets
|
||||
stay so app chrome clears display cutouts on devices with one. */
|
||||
padding: 0 env(safe-area-inset-right) 0 env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
*,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue