fix(app): make composer track clearance deterministic
Layout measurement excluded pill margin on some renderers, collapsing the intended gap above visible tracks. Keep the two-pill rail on one line and derive its complete tail clearance from shared geometry so chat switching and layout callbacks cannot change the result.
This commit is contained in:
@@ -11,6 +11,11 @@ export function resolveComposerPillClearance(isCompact: boolean): number {
|
||||
return isCompact ? COMPOSER_PILL_CLEARANCE.compact : COMPOSER_PILL_CLEARANCE.wide;
|
||||
}
|
||||
|
||||
export function resolveComposerTrackTailClearance(isCompact: boolean): number {
|
||||
const clearance = resolveComposerPillClearance(isCompact);
|
||||
return clearance + COMPOSER_PILL_MIN_HEIGHT + clearance;
|
||||
}
|
||||
|
||||
/** Shared visual contract for the compact pills immediately above the composer. */
|
||||
export const composerPillStyles = StyleSheet.create((theme) => ({
|
||||
body: {
|
||||
|
||||
@@ -12,8 +12,18 @@ vi.mock("react-native", () => ({
|
||||
},
|
||||
Pressable: "button",
|
||||
Text: "span",
|
||||
View: ({ children, style }: { children?: React.ReactNode; style?: unknown }) => (
|
||||
<div data-style={JSON.stringify(style)}>{children}</div>
|
||||
View: ({
|
||||
children,
|
||||
style,
|
||||
onLayout,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
style?: unknown;
|
||||
onLayout?: unknown;
|
||||
}) => (
|
||||
<div data-style={JSON.stringify(style)} data-has-layout-handler={Boolean(onLayout)}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -53,6 +63,7 @@ import {
|
||||
COMPOSER_PILL_CLEARANCE,
|
||||
composerPillStyles,
|
||||
resolveComposerPillClearance,
|
||||
resolveComposerTrackTailClearance,
|
||||
} from "./pill-styles";
|
||||
|
||||
describe("ComposerTrackBar", () => {
|
||||
@@ -89,8 +100,16 @@ describe("ComposerTrackBar", () => {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: { xs: 16, md: 10 },
|
||||
});
|
||||
expect(style).not.toHaveProperty("backgroundColor");
|
||||
expect(container.firstElementChild?.getAttribute("data-has-layout-handler")).toBe("false");
|
||||
|
||||
const trackStyle = JSON.parse(
|
||||
container.firstElementChild?.firstElementChild?.getAttribute("data-style") ?? "{}",
|
||||
);
|
||||
expect(trackStyle).toMatchObject({ flexDirection: "row" });
|
||||
expect(trackStyle).not.toHaveProperty("flexWrap");
|
||||
});
|
||||
|
||||
it("matches the composer's corner tangent without radius clamping", () => {
|
||||
@@ -102,6 +121,8 @@ describe("ComposerTrackBar", () => {
|
||||
expect(composerPillStyles.label).toMatchObject({ fontSize: 12 });
|
||||
expect(resolveComposerPillClearance(true)).toBe(16);
|
||||
expect(resolveComposerPillClearance(false)).toBe(10);
|
||||
expect(resolveComposerTrackTailClearance(true)).toBe(64);
|
||||
expect(resolveComposerTrackTailClearance(false)).toBe(52);
|
||||
expect(COMPOSER_PILL_CLEARANCE).toEqual({ compact: 16, wide: 10 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState, type ReactElement, type ReactNode } from "react";
|
||||
import { Pressable, Text, View, type LayoutChangeEvent } from "react-native";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import {
|
||||
MenuRoot,
|
||||
@@ -19,25 +19,16 @@ import { COMPOSER_PILL_CLEARANCE, composerPillStyles } from "./pill-styles";
|
||||
*
|
||||
* Everything in it is a pill: a count you can read without opening anything, and a panel behind
|
||||
* it for the detail. Trackers used to be stacked cards, so every one of them pushed the composer
|
||||
* further down the pane; a row of pills costs one line no matter how many there are.
|
||||
* further down the pane. The bar has at most one subagent pill and one task pill, so it remains
|
||||
* one line and has deterministic geometry on every platform.
|
||||
*
|
||||
* The bar floats over the transcript with no background, so content remains visible underneath.
|
||||
* Its host gives the scroll viewport a small bottom inset only when the bar exists; that keeps
|
||||
* the final footer clear without turning the overlay into a layout band.
|
||||
*/
|
||||
export function ComposerTrackBar({
|
||||
children,
|
||||
onHeightChange,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onHeightChange?: (height: number) => void;
|
||||
}): ReactElement {
|
||||
const handleLayout = useCallback(
|
||||
(event: LayoutChangeEvent) => onHeightChange?.(event.nativeEvent.layout.height),
|
||||
[onHeightChange],
|
||||
);
|
||||
export function ComposerTrackBar({ children }: { children: ReactNode }): ReactElement {
|
||||
return (
|
||||
<View style={styles.bar} pointerEvents="box-none" onLayout={handleLayout}>
|
||||
<View style={styles.bar} pointerEvents="box-none">
|
||||
<View style={styles.track} pointerEvents="box-none">
|
||||
{children}
|
||||
</View>
|
||||
@@ -126,7 +117,6 @@ function ComposerTrackPillTrigger({
|
||||
const pillStyle = useCallback(
|
||||
({ hovered, pressed, open: isOpen }: MenuTriggerState) => [
|
||||
composerPillStyles.body,
|
||||
styles.pillSpacing,
|
||||
(hovered || pressed || isOpen) && composerPillStyles.bodyActive,
|
||||
],
|
||||
[],
|
||||
@@ -246,21 +236,18 @@ const styles = StyleSheet.create((theme) => ({
|
||||
bottom: 0,
|
||||
alignItems: "center",
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingBottom: {
|
||||
xs: COMPOSER_PILL_CLEARANCE.compact,
|
||||
md: COMPOSER_PILL_CLEARANCE.wide,
|
||||
},
|
||||
},
|
||||
track: {
|
||||
width: "100%",
|
||||
maxWidth: MAX_CONTENT_WIDTH,
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
pillSpacing: {
|
||||
marginBottom: {
|
||||
xs: COMPOSER_PILL_CLEARANCE.compact,
|
||||
md: COMPOSER_PILL_CLEARANCE.wide,
|
||||
},
|
||||
},
|
||||
// The rail every panel row sits on: inset from the panel edge so the fill is a rounded block
|
||||
// inside it, and tall enough that revealing an action button cannot resize the row.
|
||||
row: {
|
||||
|
||||
Reference in New Issue
Block a user