mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 15:12:48 +00:00
* fix(app): dock the New workspace composer like a chat on phones Only the composer is bounded by the viewport; the setup fields sit above it in a bottom-anchored stack that moves up as the draft grows. The keyboard reservation is retained on every screen, so the composer keeps one height across keyboard transitions. Removes the per-screen reserve policy. * fix(app): share composer docking across mobile hosts Extract chat's retained capacity and platform keyboard motion so chat, workspace drafts, and New workspace share one sizing and dismissal boundary. Keep the native drivers and settled portal reconciliation unchanged. * fix(app): preserve overlays in centered composer docks Honor the optional overlay child in both layouts so changing dock placement does not silently discard part of its content. * test(app): resolve keyboard controller through its web entry The shared keyboard entry point makes stream tests load the provider. Keep its real ESM dependencies inside the Vitest resolver so CommonJS does not bypass the React Native web alias and parse native Flow sources. * fix(app): keep native chat scrolling responsive Place keyboard dismissal behind scrollable content so an idle Android Fabric JS responder cannot intercept native drags. Keep decorative form padding transparent to iOS hit testing and exercise idle native scrolling after submission from every composer host.
105 lines
3.1 KiB
TypeScript
105 lines
3.1 KiB
TypeScript
import { ComposerDockBackground } from "@/composer/dock";
|
|
import { useMemo, type ReactNode } from "react";
|
|
import type { LayoutChangeEvent } from "react-native";
|
|
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
import {
|
|
HEADER_INNER_HEIGHT,
|
|
HEADER_INNER_HEIGHT_MOBILE,
|
|
HEADER_TOP_PADDING_MOBILE,
|
|
useIsCompactFormFactor,
|
|
} from "@/constants/layout";
|
|
import { WindowChromeSafeArea } from "@/utils/desktop-window";
|
|
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
|
|
|
interface ScreenHeaderProps {
|
|
left?: ReactNode;
|
|
right?: ReactNode;
|
|
leftStyle?: StyleProp<ViewStyle>;
|
|
rightStyle?: StyleProp<ViewStyle>;
|
|
borderless?: boolean;
|
|
onRowLayout?: (event: LayoutChangeEvent) => void;
|
|
}
|
|
|
|
/**
|
|
* Shared frame for the home/back headers so we only maintain padding, border,
|
|
* and safe-area logic in one place.
|
|
*/
|
|
export function ScreenHeader({
|
|
left,
|
|
right,
|
|
leftStyle,
|
|
rightStyle,
|
|
borderless,
|
|
onRowLayout,
|
|
}: ScreenHeaderProps) {
|
|
const { theme } = useUnistyles();
|
|
const insets = useSafeAreaInsets();
|
|
const isMobile = useIsCompactFormFactor();
|
|
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
|
|
const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
|
const baseHorizontalPadding = isMobile ? theme.spacing[2] : theme.spacing[3];
|
|
|
|
const innerStyle = useMemo(
|
|
() => [styles.inner, { paddingTop: insets.top + topPadding }],
|
|
[insets.top, topPadding],
|
|
);
|
|
const rowStyle = useMemo(() => [styles.row, borderless && styles.borderless], [borderless]);
|
|
const leftCombinedStyle = useMemo(() => [styles.left, leftStyle], [leftStyle]);
|
|
const rightCombinedStyle = useMemo(() => [styles.right, rightStyle], [rightStyle]);
|
|
|
|
return (
|
|
<ComposerDockBackground style={styles.header} testID="composer-dock-header">
|
|
<View style={innerStyle}>
|
|
<WindowChromeSafeArea
|
|
placement="inline"
|
|
horizontalPadding={baseHorizontalPadding}
|
|
onLayout={onRowLayout}
|
|
style={rowStyle}
|
|
>
|
|
<TitlebarDragRegion />
|
|
<View style={leftCombinedStyle}>{left}</View>
|
|
<View style={rightCombinedStyle}>{right}</View>
|
|
</WindowChromeSafeArea>
|
|
</View>
|
|
</ComposerDockBackground>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
header: {
|
|
backgroundColor: theme.colors.surface0,
|
|
},
|
|
inner: {},
|
|
row: {
|
|
position: "relative",
|
|
height: {
|
|
xs: HEADER_INNER_HEIGHT_MOBILE,
|
|
md: HEADER_INNER_HEIGHT,
|
|
},
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
borderBottomWidth: theme.borderWidth[1],
|
|
borderBottomColor: theme.colors.border,
|
|
userSelect: "none",
|
|
},
|
|
left: {
|
|
flex: 1,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
minWidth: 0,
|
|
},
|
|
right: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
flexShrink: 0,
|
|
},
|
|
borderless: {
|
|
borderBottomColor: "transparent",
|
|
},
|
|
}));
|