Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions apps/mobile/src/components/AndroidAnchoredMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { useKeyboardState } from "react-native-keyboard-controller";
import Animated, { FadeIn } from "react-native-reanimated";

import { OverlayPortal } from "./OverlayPortal";
import { useAndroidControlSizing } from "./useAndroidControlSizing";
import { MaterialMenuPopup } from "./MaterialMenuPopup";

const MENU_WIDTH = 250;
const SCREEN_MARGIN = 12;
const ANCHOR_GAP = 6;

Expand Down Expand Up @@ -55,6 +55,7 @@ export type AndroidAnchoredMenuProps = {
* menus use the native popup for placement, animation and dismissal.
*/
export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
const { scale, menuWidth: desiredMenuWidth } = useAndroidControlSizing();
const [anchor, setAnchor] = useState<AnchorSnapshot | null>(null);
const [path, setPath] = useState<readonly MenuAction[]>([]);
// Height of the modal's root view, in the modal's own coordinate space.
Expand All @@ -68,6 +69,10 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
// are converted into this frame, so the menu lands correctly no matter
// where the portal host sits (status bar, keyboard resize, etc.).
const [overlay, setOverlay] = useState<OverlayFrame | null>(null);
const menuWidth =
overlay === null
? desiredMenuWidth
: Math.min(desiredMenuWidth, Math.max(0, overlay.width - 2 * SCREEN_MARGIN));
const anchorRef = useRef<View>(null);
const overlayRef = useRef<View>(null);

Expand Down Expand Up @@ -131,14 +136,11 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
? 0
: local.x + local.width / 2 <= overlay.width / 2
? local.x
: local.x + local.width - MENU_WIDTH;
: local.x + local.width - menuWidth;
const left =
overlay === null
? 0
: Math.min(
Math.max(preferredLeft, SCREEN_MARGIN),
overlay.width - MENU_WIDTH - SCREEN_MARGIN,
);
: Math.min(Math.max(preferredLeft, SCREEN_MARGIN), overlay.width - menuWidth - SCREEN_MARGIN);
// The keyboard stays up while the menu is open (in-window overlay, no
// focus change), so the space it covers is not usable — without this the
// composer-pill menus "open down" into the IME and can't be tapped.
Expand Down Expand Up @@ -201,6 +203,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
{!placeable || local === null ? null : !anchor.keyboardWasVisible ? (
<MaterialMenuPopup
anchor={local}
menuWidth={menuWidth}
actions={levelActions}
title={props.title}
parent={parent}
Expand All @@ -211,10 +214,12 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
) : (
<Animated.View
entering={FadeIn.duration(120)}
className="absolute w-[250px] overflow-hidden rounded-[4px] bg-card-alt shadow-md"
className="absolute overflow-hidden bg-card-alt shadow-md"
style={{
left,
maxHeight,
width: menuWidth,
borderRadius: 4 * scale,
...(opensDown
? { top: local.y + local.height + ANCHOR_GAP }
: { bottom: (rootHeight ?? 0) - local.y + ANCHOR_GAP }),
Expand All @@ -227,14 +232,15 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
active editor; the first item tap must act, not just
dismiss the keyboard. */}
<ScrollView
contentContainerClassName="py-2"
contentContainerStyle={{ paddingVertical: 7 * scale }}
bounces={false}
keyboardShouldPersistTaps="always"
showsVerticalScrollIndicator={false}
>
<MaterialMenuPopup
inline
anchor={local}
menuWidth={menuWidth}
actions={levelActions}
title={props.title}
parent={parent}
Expand Down
17 changes: 7 additions & 10 deletions apps/mobile/src/components/AndroidScreenHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { useState, type ReactNode } from "react";
import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import type { AppSymbolName } from "./AppSymbol";
import { AppText as Text } from "./AppText";
import { cn } from "../lib/cn";
import { MaterialIconButton } from "./MaterialIconButton";
import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu";
import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
import { useMaterialToolbarHeight } from "./useMaterialToolbarHeight";
import { useMaterialToolbarLayout } from "./useMaterialToolbarLayout";

export interface AndroidHeaderAction {
readonly accessibilityLabel: string;
Expand Down Expand Up @@ -38,10 +37,11 @@ export function AndroidScreenHeader(props: {
readonly embedded?: boolean;
readonly hideBottomBorder?: boolean;
}) {
const insets = useSafeAreaInsets();
const titleTypography = useScaledTextRole("title");
const subtitleTypography = useScaledTextRole("label");
const materialToolbarHeight = useMaterialToolbarHeight();
const { height: materialToolbarHeight, ...headerPadding } = useMaterialToolbarLayout(
props.embedded,
);
const [headerWidth, setHeaderWidth] = useState(0);
const actions = props.actions ?? [];
const directCount = actions.length > 2 ? (headerWidth >= 600 ? 3 : 1) : actions.length;
Expand All @@ -51,16 +51,13 @@ export function AndroidScreenHeader(props: {
return (
<View
onLayout={(event) => setHeaderWidth(event.nativeEvent.layout.width)}
className="border-b border-header-border bg-header px-2 pb-2"
className="border-b border-header-border bg-header px-2"
style={{
paddingTop: props.embedded ? 8 : Math.max(insets.top, 12),
...headerPadding,
borderBottomWidth: props.hideBottomBorder ? 0 : undefined,
}}
>
<View
style={{ minHeight: materialToolbarHeight }}
className="min-h-14 flex-row items-center gap-1"
>
<View style={{ minHeight: materialToolbarHeight }} className="flex-row items-center gap-1">
{props.onBack ? (
<MaterialIconButton
accessibilityLabel="Navigate up"
Expand Down
21 changes: 16 additions & 5 deletions apps/mobile/src/components/CompactBrandTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppText as Text } from "./AppText";
import { T3Wordmark } from "./T3Wordmark";
import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";
import { resolveMobileStageLabel } from "../lib/mobileBranding";
import { useAndroidControlSizing } from "./useAndroidControlSizing";

/**
* Horizontal correction applied to content rendered in the brand title slot,
Expand All @@ -26,6 +27,7 @@ export function CompactBrandTitle(
) {
const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
const titleOffset = brandTitleOffset();
const { scale } = useAndroidControlSizing();

return (
<View
Expand All @@ -34,19 +36,28 @@ export function CompactBrandTitle(
accessible
role="heading"
className="flex-row items-center gap-1.5"
style={{ marginLeft: titleOffset }}
style={[{ marginLeft: titleOffset }, Platform.OS === "android" && { gap: 5.25 * scale }]}
>
<T3Wordmark colorClassName="accent-icon" height={15} />
<T3Wordmark colorClassName="accent-icon" height={Math.round(15 * scale)} />
<Text
allowFontScaling={props.allowFontScaling}
className="font-t3-medium text-[21px] tracking-[-0.5px] text-foreground-muted"
className="font-t3-medium text-foreground-muted"
style={{ fontSize: 21 * scale, letterSpacing: -0.5 * scale }}
>
Code
</Text>
<View className="rounded-full bg-subtle px-1.5 py-0.5">
<View
className="rounded-full bg-subtle px-1.5 py-0.5"
style={
Platform.OS === "android"
? { paddingHorizontal: 5.25 * scale, paddingVertical: 1.75 * scale }
: undefined
}
>
<Text
allowFontScaling={props.allowFontScaling}
className="font-t3-bold text-[9px] tracking-[0.9px] text-foreground-muted uppercase"
className="font-t3-bold text-foreground-muted uppercase"
style={{ fontSize: 9 * scale, letterSpacing: 0.9 * scale }}
>
{stageLabel}
</Text>
Expand Down
4 changes: 3 additions & 1 deletion apps/mobile/src/components/ComposerAttachmentButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { MenuAction } from "@react-native-menu/menu";
import { Pressable } from "react-native";

import { useAndroidControlSizing } from "./useAndroidControlSizing";
import { SymbolView } from "./AppSymbol";
import { ControlPillMenu } from "./ControlPill";

Expand All @@ -15,6 +16,7 @@ export function ComposerAttachmentButton(props: {
readonly onPickMedia: () => Promise<void>;
readonly onPickFiles: () => Promise<void>;
}) {
const { scale } = useAndroidControlSizing();
const button = (
<Pressable
accessibilityLabel="Add attachment"
Expand All @@ -26,7 +28,7 @@ export function ComposerAttachmentButton(props: {
>
<SymbolView
name="plus"
size={20}
size={Math.round(20 * scale)}
weight="regular"
tintColorClassName="accent-icon"
type="monochrome"
Expand Down
25 changes: 18 additions & 7 deletions apps/mobile/src/components/ComposerToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ComponentProps, ReactNode } from "react";
import { useCallback, useMemo, useState } from "react";
import {
Platform,
Pressable,
ScrollView,
View,
Expand All @@ -14,6 +15,7 @@ import {
import { cn } from "../lib/cn";
import { AppText as Text } from "./AppText";
import { SymbolView } from "./AppSymbol";
import { useAndroidControlSizing } from "./useAndroidControlSizing";

const COMPOSER_TOOLBAR_GAP = 8;
const COMPOSER_TOOLBAR_FADE_WIDTH = 18;
Expand All @@ -30,7 +32,7 @@ export function ComposerInlineControl(props: {
readonly disabled?: boolean;
readonly emphasized?: boolean;
readonly icon?: ComponentProps<typeof SymbolView>["name"];
readonly iconNode?: ReactNode;
readonly renderIcon?: (size: number) => ReactNode;
readonly label: string;
readonly maxWidth?: ViewStyle["maxWidth"];
readonly onPress?: () => void;
Expand All @@ -39,6 +41,7 @@ export function ComposerInlineControl(props: {
readonly chevronDirection?: "down" | "right";
readonly showChevron?: boolean;
}) {
const { scale, smallIconSize } = useAndroidControlSizing();
return (
<Pressable
accessibilityLabel={props.accessibilityLabel ?? props.label}
Expand All @@ -52,12 +55,17 @@ export function ComposerInlineControl(props: {
onPress={props.onPress}
style={{ maxWidth: props.maxWidth ?? 190, opacity: props.disabled ? 0.45 : 1 }}
>
{props.iconNode ? (
<View className="size-4 shrink-0 items-center justify-center">{props.iconNode}</View>
{props.renderIcon ? (
<View
className="size-4 shrink-0 items-center justify-center"
style={Platform.OS === "android" ? { width: 14 * scale, height: 14 * scale } : undefined}
>
{props.renderIcon(smallIconSize)}
</View>
) : props.icon ? (
<SymbolView
name={props.icon}
size={16}
size={smallIconSize}
tintColorClassName={
props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted"
}
Expand All @@ -76,7 +84,7 @@ export function ComposerInlineControl(props: {
{props.showChevron === false ? null : (
<SymbolView
name={props.chevronDirection === "right" ? "chevron.right" : "chevron.down"}
size={10}
size={Math.round(10 * scale)}
tintColorClassName={
props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted"
}
Expand Down Expand Up @@ -224,6 +232,8 @@ export function ComposerActionButton(props: {
readonly onPress: () => void;
readonly variant?: "primary" | "danger";
}) {
const { scale, smallIconSize } = useAndroidControlSizing();
const circleSize = Math.round(30 * scale);
return (
<Pressable
accessibilityLabel={props.accessibilityLabel}
Expand All @@ -234,8 +244,9 @@ export function ComposerActionButton(props: {
onPress={props.onPress}
>
<View
style={{ width: circleSize, height: circleSize }}
className={cn(
"size-[30px] items-center justify-center rounded-full",
"items-center justify-center rounded-full",
props.variant === "danger"
? "bg-danger"
: props.disabled
Expand All @@ -245,7 +256,7 @@ export function ComposerActionButton(props: {
>
<SymbolView
name={props.icon}
size={16}
size={smallIconSize}
weight="semibold"
tintColorClassName={
props.variant === "danger" ? "accent-danger-foreground" : "accent-primary-foreground"
Expand Down
4 changes: 3 additions & 1 deletion apps/mobile/src/components/ControlPill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SymbolView } from "./AppSymbol";
import { AppText as Text } from "./AppText";
import { MaterialIconButton } from "./MaterialIconButton";
import { MaterialButton } from "./MaterialButton";
import { useAndroidControlSizing } from "./useAndroidControlSizing";

export { ControlPillMenu } from "./ControlPillMenu";

Expand All @@ -20,6 +21,7 @@ export function ControlPill(props: {
readonly className?: string;
}) {
const variant = props.variant ?? "circle";
const { smallIconSize } = useAndroidControlSizing();
const activatedOnPressInRef = useRef(false);

const handlePressIn = () => {
Expand Down Expand Up @@ -133,7 +135,7 @@ export function ControlPill(props: {
) : props.icon ? (
<SymbolView
name={props.icon}
size={16}
size={smallIconSize}
tintColorClassName={iconTintClassName}
type="monochrome"
/>
Expand Down
6 changes: 4 additions & 2 deletions apps/mobile/src/components/MaterialButton.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import { View } from "react-native";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
import type { MaterialButtonProps } from "./MaterialButton";
import { useAndroidControlSizing } from "./useAndroidControlSizing";

export function MaterialButton(props: MaterialButtonProps) {
const { themeAppearance, themeVariables: colors } = useAppearancePreferences();
const typography = useScaledTextRole("footnote");
const { scale, mediumIconSize } = useAndroidControlSizing();
const tone = props.tone ?? "secondary";
const Component =
tone === "text" ? TextButton : tone === "secondary" ? FilledTonalButton : Button;
Expand Down Expand Up @@ -77,11 +79,11 @@ export function MaterialButton(props: MaterialButtonProps) {
{props.loading ? (
<>
<CircularProgressIndicator
modifiers={[size(18, 18)]}
modifiers={[size(mediumIconSize, mediumIconSize)]}
strokeWidth={2}
color={colors["--color-foreground-muted"]}
/>
<Box modifiers={[size(8, 1)]} />
<Box modifiers={[size(8 * scale, 1)]} />
</>
) : null}
<Text style={{ ...typography, fontWeight: "500" }}>{props.label}</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
LargeFloatingActionButton,
Text,
} from "@expo/ui/jetpack-compose";
import { size } from "@expo/ui/jetpack-compose/modifiers";
import { defaultMinSize, height, size, width } from "@expo/ui/jetpack-compose/modifiers";
import { View, type StyleProp, type ViewStyle } from "react-native";
import { useAndroidControlSizing } from "./useAndroidControlSizing";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
import { SymbolView, type AppSymbolName } from "./AppSymbol";
Expand All @@ -24,6 +25,8 @@ export function MaterialFloatingActionButton(props: {
}) {
const { themeAppearance, themeVariables: colors } = useAppearancePreferences();
const typography = useScaledTextRole("footnote");
const { scale, iconSize: standardIconSize, fabSize, largeFabSize } = useAndroidControlSizing();
const buttonSize = props.variant === "large" ? largeFabSize : fabSize;
const primary = props.tone === "primary";
const containerColor = colors[primary ? "--color-primary" : "--color-secondary"];
const contentColor =
Expand All @@ -34,7 +37,7 @@ export function MaterialFloatingActionButton(props: {
: props.variant === "large"
? LargeFloatingActionButton
: FloatingActionButton;
const iconSize = props.variant === "large" ? 36 : 24;
const iconSize = props.variant === "large" ? Math.round(36 * scale) : standardIconSize;
return (
<View
accessible
Expand All @@ -48,6 +51,11 @@ export function MaterialFloatingActionButton(props: {
<View importantForAccessibility="no-hide-descendants">
<Host matchContents colorScheme={themeAppearance} ignoreSafeAreaKeyboardInsets>
<Component
modifiers={[
defaultMinSize({ minWidth: buttonSize }),
height(buttonSize),
...(props.variant === "extended" ? [] : [width(buttonSize)]),
]}
containerColor={containerColor}
onClick={props.onPress}
expanded={props.expanded}
Expand Down
Loading
Loading