Files
paseo/packages/app/src/window-layout/model.ts
T
Mohamed BoudraandElsa Granger e9f897a66a fix(app): rest the iPhone composer at the screen's bottom clearance (#8823)
* fix(app): rest the iPhone composer at the screen's bottom clearance

The composer sat 50pt above the bottom edge on every iPhone: the dock
padded the rectangular home-indicator safe area (34pt) and the composer
stacked another 16pt under it. On iPhone Duo that safe area is also
taller than needed; iOS 27.1's bottom-bar layout guide, which follows
the screen's corners, rests 26pt from the edge.

The native window-layout probe now publishes that clearance, and
WindowLayoutProvider overrides the safe-area context's bottom with it,
so every useSafeAreaInsets() consumer reads one bottom value and null
falls back to the system inset. The docked composer applies
max(bottom, 16) once and keeps 16pt above the keyboard through the
translate view's transform; the gap is not animated as padding because
layout updates through the keyboard transition are expensive on Android
Fabric.

Co-authored-by: Elsa Granger <zeyugao@outlook.com>

* fix(app): keep the Import session gap under the wide new workspace composer

The composer's own bottom spacing used to separate it from the Import
session pill on the wide form. With that spacing moved to the dock, the
centered slot padded below the pill instead, so the pill touched the
composer. The pill now owns its gap above.

Co-authored-by: Elsa Granger <zeyugao@outlook.com>

---------

Co-authored-by: Elsa Granger <zeyugao@outlook.com>
2026-11-05 21:33:17 +07:00

82 lines
2.5 KiB
TypeScript

export interface WindowLayoutRect {
x: number;
y: number;
width: number;
height: number;
}
export interface WindowLayout {
bounds: WindowLayoutRect;
verticalBarEdge: "none" | "leading" | "trailing";
verticalBar: WindowLayoutRect | null;
/**
* Points from the window's bottom edge to where a bottom bar rests, or null where
* the system has no bottom bar guide. Zero is a measured value.
*/
bottomClearance?: number | null;
divisions: WindowLayoutRect[];
}
/** The window's measured bottom clearance replaces the rectangular safe-area bottom. */
export function resolveSafeAreaInsets<Insets extends { bottom: number }>(
systemInsets: Insets,
layout: WindowLayout | null,
): Insets {
const clearance = layout?.bottomClearance;
return typeof clearance === "number" && clearance !== systemInsets.bottom
? { ...systemInsets, bottom: clearance }
: systemInsets;
}
export interface DesktopDivisionLayout {
sidebarWidth: number;
gap: number;
contentWidth: number;
}
export function resolveWorkspaceActionPlacement(
layout: WindowLayout | null,
): "header" | "vertical-bar" {
return layout?.verticalBarEdge === "trailing" && layout.verticalBar !== null
? "vertical-bar"
: "header";
}
export function resolveWorkspaceBarActions(
headerVariant: "compact" | "full",
): readonly ("tab-count" | "overflow" | "explorer")[] {
return headerVariant === "compact"
? ["tab-count", "overflow", "explorer"]
: ["overflow", "explorer"];
}
/** One book-like partition owns allocation; horizontal folds retain the normal flow. */
export function resolveDesktopDivisionLayout(input: {
geometry: WindowLayout | null;
windowWidth: number;
windowHeight: number;
leftInset: number;
rightInset: number;
}): DesktopDivisionLayout | null {
const { geometry, windowWidth, windowHeight, leftInset, rightInset } = input;
if (
!geometry ||
Math.abs(geometry.bounds.width - windowWidth) > 0.5 ||
Math.abs(geometry.bounds.height - windowHeight) > 0.5
)
return null;
const divisions = geometry.divisions.filter(
(frame) =>
frame.width > 0 &&
frame.height > frame.width &&
frame.y < windowHeight &&
frame.y + frame.height > 0,
);
if (divisions.length !== 1) return null;
const division = divisions[0];
const sidebarWidth = division.x - leftInset;
const contentWidth = windowWidth - rightInset - division.x - division.width;
if (sidebarWidth <= 0 || contentWidth <= 0) return null;
return { sidebarWidth, gap: division.width, contentWidth };
}