diff --git a/apps/mobile/assets/icons/chevron-down.xml b/apps/mobile/assets/icons/chevron-down.xml
new file mode 100644
index 0000000000000..285eda9c1b463
--- /dev/null
+++ b/apps/mobile/assets/icons/chevron-down.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/apps/mobile/src/components/ui/MenuPopup.android.tsx b/apps/mobile/src/components/ui/MenuPopup.android.tsx
index b5af18cd2bcbb..dd6ef2b046566 100644
--- a/apps/mobile/src/components/ui/MenuPopup.android.tsx
+++ b/apps/mobile/src/components/ui/MenuPopup.android.tsx
@@ -55,7 +55,9 @@ function MenuIcon(props: {
);
}
-export function MenuPopup(props: MenuPopupProps) {
+export function MenuPopupItems(
+ props: Pick,
+) {
const { appearance, themeVariables: colors } = useAppearancePreferences();
const { scale, menuItemHeight } = useAndroidControlSizing();
const body = resolveScaledTextRole("body", appearance.baseFontSize);
@@ -64,7 +66,7 @@ export function MenuPopup(props: MenuPopupProps) {
const muted = colors["--color-foreground-muted"];
// A fixed native item height clips wrapped labels; a minimum lets each row grow.
const itemModifiers = [width(props.menuWidth), defaultMinSize({ minHeight: menuItemHeight })];
- const items = (
+ return (
<>
{props.parent ? (
@@ -146,6 +148,11 @@ export function MenuPopup(props: MenuPopupProps) {
))}
>
);
+}
+
+export function MenuPopup(props: MenuPopupProps) {
+ const { themeVariables: colors } = useAppearancePreferences();
+ const items = ;
if (props.inline) {
return (
;
+ const enabled = !props.disabled && !props.loading;
+ const menuEnabled = props.actions.some((action) => !action.attributes?.disabled);
+ const buttonColors = {
+ containerColor: colors["--color-primary"],
+ contentColor: colors["--color-primary-foreground"],
+ disabledContainerColor: colors["--color-subtle-strong"],
+ disabledContentColor: colors["--color-foreground-muted"],
+ };
+ // A usable menu beside a disabled action still takes the disabled palette, so the pair reads as one control.
+ const trailingColors = enabled
+ ? buttonColors
+ : {
+ ...buttonColors,
+ containerColor: buttonColors.disabledContainerColor,
+ contentColor: buttonColors.disabledContentColor,
+ };
+ const label = (
+
+ {props.loading ? (
+ <>
+
+
+ >
+ ) : props.icon ? (
+ <>
+ {/* The glyph host consumes touches, so it runs the action itself. */}
+
+
+
+
+
+
+ >
+ ) : null}
+ {props.label}
+
+ );
+ return (
+
+
+
+ {label}
+
+
+ setExpanded(false)}
+ color={colors["--color-card-alt"]}
+ >
+
+
+
+
+ {
+ setExpanded(false);
+ if (action.id !== undefined) props.onPressAction(action.id);
+ }}
+ onBack={() => {}}
+ />
+
+
+
+
+
+ );
+}
diff --git a/apps/mobile/src/components/ui/SplitButton.tsx b/apps/mobile/src/components/ui/SplitButton.tsx
new file mode 100644
index 0000000000000..511c3a5b7c29f
--- /dev/null
+++ b/apps/mobile/src/components/ui/SplitButton.tsx
@@ -0,0 +1,65 @@
+import { Pressable, View } from "react-native";
+import { AppActivityIndicator } from "./AppActivityIndicator";
+import { AppText } from "./AppText";
+import { SymbolView } from "./AppSymbol";
+import { Menu } from "./Menu";
+import type { SplitButtonProps } from "./SplitButton.types";
+
+export function SplitButton(props: SplitButtonProps) {
+ const active = !props.disabled;
+ const menuEnabled = props.actions.some((action) => !action.attributes?.disabled);
+ return (
+
+
+ {props.loading ? (
+
+ ) : props.icon ? (
+
+ ) : null}
+
+ {props.label}
+
+
+ {props.actions.length > 0 ? (
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/apps/mobile/src/components/ui/SplitButton.types.ts b/apps/mobile/src/components/ui/SplitButton.types.ts
new file mode 100644
index 0000000000000..73d0b36f2ce20
--- /dev/null
+++ b/apps/mobile/src/components/ui/SplitButton.types.ts
@@ -0,0 +1,14 @@
+import type { MenuAction } from "@react-native-menu/menu";
+import type { AppSymbolName } from "./AppSymbol";
+
+export interface SplitButtonProps {
+ readonly label: string;
+ readonly accessibilityLabel?: string;
+ readonly icon?: AppSymbolName;
+ readonly disabled?: boolean;
+ readonly loading?: boolean;
+ readonly onPress: () => void;
+ readonly menuTitle: string;
+ readonly actions: MenuAction[];
+ readonly onPressAction: (id: string) => void;
+}
diff --git a/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx b/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx
index b784bdb4eb427..8ba68d2a2bfa9 100644
--- a/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx
+++ b/apps/mobile/src/features/pull-requests/PullRequestMergeControls.tsx
@@ -1,4 +1,3 @@
-import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator";
import * as Dialog from "../../components/ui/Dialog";
import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react";
import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
@@ -6,12 +5,10 @@ import { sourceControlClients } from "@t3tools/client-runtime/source-control-cli
import type { PullRequestDetail, PullRequestMergeMethod } from "@t3tools/contracts";
import { AsyncResult } from "effect/reactivity";
import { useRef, useState } from "react";
-import { Platform, Pressable, View } from "react-native";
+import { View } from "react-native";
import { AppText as Text } from "../../components/ui/AppText";
-import { SymbolView } from "../../components/ui/AppSymbol";
-import { Menu } from "../../components/ui/Menu";
import { Button } from "../../components/ui/Button";
-import { IconButton } from "../../components/ui/IconButton";
+import { SplitButton } from "../../components/ui/SplitButton";
import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
import { composerPullRequests } from "../../state/pull-requests";
import { useAtomCommand } from "../../state/use-atom-command";
@@ -26,8 +23,8 @@ import {
import { pullRequestStackMerge, singlePullRequestMergeAllowed } from "./pullRequestStack";
import { pullRequestTarget, type PullRequestParams } from "./usePullRequestDiff";
import { usePullRequestPermissions } from "./usePullRequestPermissions";
-import type { PullRequestStackState } from "./usePullRequestStack";
import { PullRequestGlyph } from "./pullRequestGlyphs";
+import type { PullRequestStackState } from "./usePullRequestStack";
export function PullRequestMergeControls(props: {
readonly pr: PullRequestDetail;
@@ -169,107 +166,41 @@ export function PullRequestMergeControls(props: {
};
return (
-
- {Platform.OS === "android" ? (
-
-
- ) : (
- confirm("merge")}
- className="min-h-11 min-w-0 flex-1 flex-row items-center justify-center gap-2 px-3 py-2 active:opacity-70"
- >
- {busy || checkingStack ? (
-
- ) : (
-
- )}
-
- {busy ? "Working…" : !permissions.connected ? "Reconnect to merge" : label}
-
-
- )}
- {methods.length > 0 || canAutoMerge ? (
-
- ) : null}
-
+ icon={PullRequestGlyph.merged}
+ disabled={!canMerge}
+ loading={busy || checkingStack}
+ onPress={() => confirm("merge")}
+ menuTitle="Merge options"
+ actions={[
+ ...methods.map((id) => ({
+ id,
+ title: MERGE_METHODS[id].title,
+ state: id === method ? ("on" as const) : ("off" as const),
+ attributes: { disabled: !canMerge && !canAutoMerge },
+ })),
+ ...(canAutoMerge
+ ? [
+ {
+ id: autoAction,
+ title: pr.autoMergeEnabled ? "Disable auto-merge" : "Enable auto-merge",
+ image: "clock",
+ },
+ ]
+ : []),
+ ]}
+ onPressAction={(id) => {
+ const selected = methods.find((method) => method === id);
+ if (selected) {
+ setSelectedMethod(selected);
+ savePreferences({ pullRequestMergeMethod: selected });
+ } else if (id === autoAction && canAutoMerge) confirm(autoAction);
+ }}
+ />
{permissions.connected &&
reason &&
reason !== MERGE_CONFLICT_REASON &&
diff --git a/patches/@expo__ui@58.0.20.patch b/patches/@expo__ui@58.0.20.patch
index 2546d83b80428..859ee062b9fdf 100644
--- a/patches/@expo__ui@58.0.20.patch
+++ b/patches/@expo__ui@58.0.20.patch
@@ -1,8 +1,49 @@
diff --git a/android/src/main/java/expo/modules/ui/ExpoUIModule.kt b/android/src/main/java/expo/modules/ui/ExpoUIModule.kt
-index d58b54f..1920b1a 100644
+index d58b54fc30ba0f9e5bc35b505ae598161a59b9b1..2da4df803a918d034921d41d49d92121e6303f82 100644
--- a/android/src/main/java/expo/modules/ui/ExpoUIModule.kt
+++ b/android/src/main/java/expo/modules/ui/ExpoUIModule.kt
-@@ -533,6 +533,10 @@ class ExpoUIModule : Module() {
+@@ -29,6 +29,13 @@ import expo.modules.ui.button.IconButtonContent
+ import expo.modules.ui.button.FilledIconButtonContent
+ import expo.modules.ui.button.FilledTonalIconButtonContent
+ import expo.modules.ui.button.OutlinedIconButtonContent
++import expo.modules.ui.button.SplitButtonCheckedChangeEvent
++import expo.modules.ui.button.SplitButtonLayoutContent
++import expo.modules.ui.button.SplitButtonLayoutProps
++import expo.modules.ui.button.SplitButtonLeadingButtonContent
++import expo.modules.ui.button.SplitButtonLeadingButtonProps
++import expo.modules.ui.button.SplitButtonTrailingButtonContent
++import expo.modules.ui.button.SplitButtonTrailingButtonProps
+ import expo.modules.ui.graphics.ImageLoader
+ import expo.modules.ui.icon.IconView
+ import expo.modules.ui.image.ImageView
+@@ -294,6 +301,26 @@ class ExpoUIModule : Module() {
+ }
+ }
+
++ ExpoUIView("SplitButtonLayoutView") {
++ Content { props -> SplitButtonLayoutContent(props) }
++ }
++
++ ExpoUIView("SplitButtonLeadingButtonView") {
++ val onButtonPressed by Event()
++
++ Content { props ->
++ SplitButtonLeadingButtonContent(props) { onButtonPressed(Unit) }
++ }
++ }
++
++ ExpoUIView("SplitButtonTrailingButtonView") {
++ val onCheckedChange by Event()
++
++ Content { props ->
++ SplitButtonTrailingButtonContent(props) { onCheckedChange(it) }
++ }
++ }
++
+ ExpoUIView("FilledTonalButton") {
+ val onButtonPressed by Event()
+
+@@ -533,6 +560,10 @@ class ExpoUIModule : Module() {
}
}
@@ -14,7 +55,7 @@ index d58b54f..1920b1a 100644
Content { props ->
HorizontalFloatingToolbarContent(props)
diff --git a/android/src/main/java/expo/modules/ui/HostView.kt b/android/src/main/java/expo/modules/ui/HostView.kt
-index 4887034..6eaf039 100644
+index 4887034bf7e3e6b5d8d2c1ae25932f5a24f9435d..6eaf039a14820ddaf635a1538c2a9a644cf704fb 100644
--- a/android/src/main/java/expo/modules/ui/HostView.kt
+++ b/android/src/main/java/expo/modules/ui/HostView.kt
@@ -69,6 +69,7 @@ internal enum class ExpoLayoutDirection(val value: String) : Enumerable {
@@ -89,7 +130,7 @@ index 4887034..6eaf039 100644
// Material3's `MaterialTheme` does not provide `LocalContentColor` — only `Surface` does — so
diff --git a/android/src/main/java/expo/modules/ui/TopAppBarView.kt b/android/src/main/java/expo/modules/ui/TopAppBarView.kt
new file mode 100644
-index 0000000..4ed0132
+index 0000000000000000000000000000000000000000..4ed0132d57fc7aff08ecb7ee84fb8fa48942931b
--- /dev/null
+++ b/android/src/main/java/expo/modules/ui/TopAppBarView.kt
@@ -0,0 +1,47 @@
@@ -140,8 +181,109 @@ index 0000000..4ed0132
+ modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
+ )
+}
+diff --git a/android/src/main/java/expo/modules/ui/button/SplitButton.kt b/android/src/main/java/expo/modules/ui/button/SplitButton.kt
+new file mode 100644
+index 0000000000000000000000000000000000000000..10df003066fb0703645c4306bd557590b2cae9aa
+--- /dev/null
++++ b/android/src/main/java/expo/modules/ui/button/SplitButton.kt
+@@ -0,0 +1,95 @@
++@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
++
++package expo.modules.ui.button
++
++import androidx.compose.material3.ButtonDefaults
++import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
++import androidx.compose.material3.SplitButtonDefaults
++import androidx.compose.material3.SplitButtonLayout
++import androidx.compose.runtime.Composable
++import expo.modules.kotlin.records.Field
++import expo.modules.kotlin.records.Record
++import expo.modules.kotlin.types.OptimizedRecord
++import expo.modules.kotlin.views.ComposeProps
++import expo.modules.kotlin.views.FunctionalComposableScope
++import expo.modules.kotlin.views.OptimizedComposeProps
++import expo.modules.ui.ModifierList
++import expo.modules.ui.ModifierRegistry
++import expo.modules.ui.UIComposableScope
++import expo.modules.ui.compose
++import expo.modules.ui.isSlotWithName
++
++@OptimizedComposeProps
++data class SplitButtonLayoutProps(
++ val modifiers: ModifierList = emptyList()
++) : ComposeProps
++
++@OptimizedComposeProps
++data class SplitButtonLeadingButtonProps(
++ val colors: ButtonColors = ButtonColors(),
++ val enabled: Boolean = true,
++ val modifiers: ModifierList = emptyList()
++) : ComposeProps
++
++@OptimizedComposeProps
++data class SplitButtonTrailingButtonProps(
++ val checked: Boolean = false,
++ val colors: ButtonColors = ButtonColors(),
++ val enabled: Boolean = true,
++ val modifiers: ModifierList = emptyList()
++) : ComposeProps
++
++@OptimizedRecord
++data class SplitButtonCheckedChangeEvent(
++ @Field val checked: Boolean = false
++) : Record
++
++@Composable
++fun FunctionalComposableScope.SplitButtonLayoutContent(props: SplitButtonLayoutProps) {
++ SplitButtonLayout(
++ leadingButton = { Children(UIComposableScope(), filter = { isSlotWithName(it, "leadingButton") }) },
++ trailingButton = { Children(UIComposableScope(), filter = { isSlotWithName(it, "trailingButton") }) },
++ modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
++ )
++}
++
++@Composable
++fun FunctionalComposableScope.SplitButtonLeadingButtonContent(
++ props: SplitButtonLeadingButtonProps,
++ onClick: () -> Unit
++) {
++ SplitButtonDefaults.LeadingButton(
++ onClick = onClick,
++ enabled = props.enabled,
++ colors = ButtonDefaults.buttonColors(
++ containerColor = props.colors.containerColor.compose,
++ contentColor = props.colors.contentColor.compose,
++ disabledContainerColor = props.colors.disabledContainerColor.compose,
++ disabledContentColor = props.colors.disabledContentColor.compose
++ ),
++ modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
++ ) {
++ Children(UIComposableScope(rowScope = this))
++ }
++}
++
++@Composable
++fun FunctionalComposableScope.SplitButtonTrailingButtonContent(
++ props: SplitButtonTrailingButtonProps,
++ onCheckedChange: (SplitButtonCheckedChangeEvent) -> Unit
++) {
++ SplitButtonDefaults.TrailingButton(
++ checked = props.checked,
++ onCheckedChange = { onCheckedChange(SplitButtonCheckedChangeEvent(it)) },
++ enabled = props.enabled,
++ colors = ButtonDefaults.buttonColors(
++ containerColor = props.colors.containerColor.compose,
++ contentColor = props.colors.contentColor.compose,
++ disabledContainerColor = props.colors.disabledContainerColor.compose,
++ disabledContentColor = props.colors.disabledContentColor.compose
++ ),
++ modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
++ ) {
++ Children(UIComposableScope(rowScope = this))
++ }
++}
diff --git a/build/jetpack-compose/Host/index.d.ts b/build/jetpack-compose/Host/index.d.ts
-index b586b65..e32f6e0 100644
+index b586b653a991ce8840b9994a6fe1e6af75480ece..e32f6e093fde22e9cfc9d7ff612cddb59e036945 100644
--- a/build/jetpack-compose/Host/index.d.ts
+++ b/build/jetpack-compose/Host/index.d.ts
@@ -1,6 +1,7 @@
@@ -161,7 +303,7 @@ index b586b65..e32f6e0 100644
* The layout direction for the content.
* Defaults to the current locale direction from I18nManager.
diff --git a/build/jetpack-compose/Host/index.js b/build/jetpack-compose/Host/index.js
-index 3985656..2b4a4f9 100644
+index 39856563a8cf1119f3a72e37fd35d48d245041bf..2b4a4f946f2ff04a5563c4cd8a29f28b0c5cc08b 100644
--- a/build/jetpack-compose/Host/index.js
+++ b/build/jetpack-compose/Host/index.js
@@ -8,16 +8,17 @@ import { useMergeRefs } from '../../utils/useMergeRefs';
@@ -194,9 +336,141 @@ index 3985656..2b4a4f9 100644
onLayoutContent: onLayoutContent,
layoutDirection: layoutDirection ?? (I18nManager.getConstants().isRTL ? 'rightToLeft' : 'leftToRight'),
ref: mergedRef,
+diff --git a/build/jetpack-compose/SplitButton/index.d.ts b/build/jetpack-compose/SplitButton/index.d.ts
+new file mode 100644
+index 0000000000000000000000000000000000000000..4c228bd16b10c8d08d96c96bb143e07769d06aa6
+--- /dev/null
++++ b/build/jetpack-compose/SplitButton/index.d.ts
+@@ -0,0 +1,73 @@
++import type { ReactNode } from 'react';
++import type { ModifierConfig } from '../../types';
++import type { ButtonColors } from '../Button';
++export interface SplitButtonLayoutProps {
++ /**
++ * A `SplitButtonLayout.LeadingButton` and a `SplitButtonLayout.TrailingButton`.
++ */
++ children: ReactNode;
++ /**
++ * Modifiers for the component.
++ */
++ modifiers?: ModifierConfig[];
++}
++export interface SplitButtonLeadingButtonProps {
++ /**
++ * Callback that is called when the leading button is clicked.
++ */
++ onClick?: () => void;
++ /**
++ * Whether the button is enabled for user interaction.
++ * @default true
++ */
++ enabled?: boolean;
++ /**
++ * Colors for button elements.
++ */
++ colors?: ButtonColors;
++ /**
++ * Modifiers for the component.
++ */
++ modifiers?: ModifierConfig[];
++ /**
++ * Content to display inside the button.
++ */
++ children: ReactNode;
++}
++export interface SplitButtonTrailingButtonProps {
++ /**
++ * Whether the trailing button is checked, usually while its menu is expanded.
++ */
++ checked: boolean;
++ /**
++ * Callback that is called when the checked state changes.
++ */
++ onCheckedChange?: (checked: boolean) => void;
++ /**
++ * Whether the button is enabled for user interaction.
++ * @default true
++ */
++ enabled?: boolean;
++ /**
++ * Colors for button elements.
++ */
++ colors?: ButtonColors;
++ /**
++ * Modifiers for the component.
++ */
++ modifiers?: ModifierConfig[];
++ /**
++ * Content to display inside the button, usually a chevron icon.
++ */
++ children: ReactNode;
++}
++/**
++ * A Material 3 split button: a leading action button and a trailing toggle button,
++ * usually anchoring a `DropdownMenu` of related actions.
++ */
++declare function SplitButtonLayout(props: SplitButtonLayoutProps): import("react/jsx-runtime").JSX.Element;
++declare namespace SplitButtonLayout {
++ var LeadingButton: (props: SplitButtonLeadingButtonProps) => import("react/jsx-runtime").JSX.Element;
++ var TrailingButton: (props: SplitButtonTrailingButtonProps) => import("react/jsx-runtime").JSX.Element;
++}
++export { SplitButtonLayout };
+diff --git a/build/jetpack-compose/SplitButton/index.js b/build/jetpack-compose/SplitButton/index.js
+new file mode 100644
+index 0000000000000000000000000000000000000000..0e723b1e28d30ba0410437eaa5a1a9a0f50e6ad4
+--- /dev/null
++++ b/build/jetpack-compose/SplitButton/index.js
+@@ -0,0 +1,47 @@
++import { jsx as _jsx } from "react/jsx-runtime";
++import { requireNativeView } from 'expo';
++import { createViewModifierEventListener } from '../modifiers/utils';
++const SplitButtonLayoutNativeView = requireNativeView('ExpoUI', 'SplitButtonLayoutView');
++const LeadingButtonNativeView = requireNativeView('ExpoUI', 'SplitButtonLeadingButtonView');
++const TrailingButtonNativeView = requireNativeView('ExpoUI', 'SplitButtonTrailingButtonView');
++const SlotNativeView = requireNativeView('ExpoUI', 'SlotView');
++function LeadingButton(props) {
++ const { onClick, modifiers, ...restProps } = props;
++ return /*#__PURE__*/ _jsx(SlotNativeView, {
++ slotName: "leadingButton",
++ children: /*#__PURE__*/ _jsx(LeadingButtonNativeView, {
++ modifiers: modifiers,
++ ...modifiers ? createViewModifierEventListener(modifiers) : undefined,
++ ...restProps,
++ enabled: props.enabled ?? true,
++ onButtonPressed: onClick ? ()=>onClick() : undefined
++ })
++ });
++}
++function TrailingButton(props) {
++ const { onCheckedChange, modifiers, ...restProps } = props;
++ return /*#__PURE__*/ _jsx(SlotNativeView, {
++ slotName: "trailingButton",
++ children: /*#__PURE__*/ _jsx(TrailingButtonNativeView, {
++ modifiers: modifiers,
++ ...modifiers ? createViewModifierEventListener(modifiers) : undefined,
++ ...restProps,
++ enabled: props.enabled ?? true,
++ onCheckedChange: onCheckedChange ? ({ nativeEvent: { checked } })=>onCheckedChange(checked) : undefined
++ })
++ });
++}
++/**
++ * A Material 3 split button: a leading action button and a trailing toggle button,
++ * usually anchoring a `DropdownMenu` of related actions.
++ */ function SplitButtonLayout(props) {
++ const { modifiers, ...restProps } = props;
++ return /*#__PURE__*/ _jsx(SplitButtonLayoutNativeView, {
++ modifiers: modifiers,
++ ...modifiers ? createViewModifierEventListener(modifiers) : undefined,
++ ...restProps
++ });
++}
++SplitButtonLayout.LeadingButton = LeadingButton;
++SplitButtonLayout.TrailingButton = TrailingButton;
++export { SplitButtonLayout };
diff --git a/build/jetpack-compose/TopAppBar/index.d.ts b/build/jetpack-compose/TopAppBar/index.d.ts
new file mode 100644
-index 0000000..63afa28
+index 0000000000000000000000000000000000000000..63afa28d56a9b0260de16d32e08bd7405e2c517d
--- /dev/null
+++ b/build/jetpack-compose/TopAppBar/index.d.ts
@@ -0,0 +1,15 @@
@@ -217,7 +491,7 @@ index 0000000..63afa28
+}
diff --git a/build/jetpack-compose/TopAppBar/index.js b/build/jetpack-compose/TopAppBar/index.js
new file mode 100644
-index 0000000..c6557fe
+index 0000000000000000000000000000000000000000..c6557fe6cb559c7d922fc02ce25fa2bf080a50d6
--- /dev/null
+++ b/build/jetpack-compose/TopAppBar/index.js
@@ -0,0 +1,11 @@
@@ -233,7 +507,7 @@ index 0000000..c6557fe
+TopAppBar.NavigationIcon = function NavigationIcon({ children }) { return jsx(Slot, { style: { position: 'absolute' }, slotName: 'navigationIcon', children }); };
+TopAppBar.Actions = function Actions({ children }) { return jsx(Slot, { style: { position: 'absolute' }, slotName: 'actions', children }); };
diff --git a/build/jetpack-compose/index.d.ts b/build/jetpack-compose/index.d.ts
-index fa70b35..436a8b7 100644
+index fa70b3574c4c40753b683a98c053caaf63e0385b..96fbbbeaba8396cc9c5cf255c61991bbb2afa899 100644
--- a/build/jetpack-compose/index.d.ts
+++ b/build/jetpack-compose/index.d.ts
@@ -1,3 +1,4 @@
@@ -241,8 +515,16 @@ index fa70b35..436a8b7 100644
import './MaterialSymbolsAssetsTransformer.fx';
export * from './AlertDialog';
export * from './Badge';
+@@ -21,6 +22,7 @@ export * from './ListItem';
+ export * from './RNHostView';
+ export * from './DatePicker';
+ export * from './SegmentedButton';
++export * from './SplitButton';
+ export * from './Progress';
+ export * from './Slider';
+ export * from './Spacer';
diff --git a/build/jetpack-compose/index.js b/build/jetpack-compose/index.js
-index 2457ed1..fdc8eb1 100644
+index 2457ed1be68b4295442f24a6175fa347cbf80cdb..f394321920fbf1ebb6c7c68bb46690538a05aa0d 100644
--- a/build/jetpack-compose/index.js
+++ b/build/jetpack-compose/index.js
@@ -1,3 +1,4 @@
@@ -250,8 +532,16 @@ index 2457ed1..fdc8eb1 100644
import './MaterialSymbolsAssetsTransformer.fx';
export * from './AlertDialog';
export * from './Badge';
+@@ -21,6 +22,7 @@ export * from './ListItem';
+ export * from './RNHostView';
+ export * from './DatePicker';
+ export * from './SegmentedButton';
++export * from './SplitButton';
+ export * from './Progress';
+ export * from './Slider';
+ export * from './Spacer';
diff --git a/src/jetpack-compose/Host/index.tsx b/src/jetpack-compose/Host/index.tsx
-index e994f6e..d0ed70a 100644
+index e994f6e1fbad28414207482deeceb542a7b95746..d0ed70a3b9ab0ce5e5c1fd9106c0759f181b6e96 100644
--- a/src/jetpack-compose/Host/index.tsx
+++ b/src/jetpack-compose/Host/index.tsx
@@ -12,7 +12,7 @@ import {
@@ -300,9 +590,157 @@ index e994f6e..d0ed70a 100644
onLayoutContent={onLayoutContent}
layoutDirection={
layoutDirection ?? (I18nManager.getConstants().isRTL ? 'rightToLeft' : 'leftToRight')
+diff --git a/src/jetpack-compose/SplitButton/index.tsx b/src/jetpack-compose/SplitButton/index.tsx
+new file mode 100644
+index 0000000000000000000000000000000000000000..05cba2bfa15b553f74ccacac0b889fae73b13007
+--- /dev/null
++++ b/src/jetpack-compose/SplitButton/index.tsx
+@@ -0,0 +1,142 @@
++import { requireNativeView } from 'expo';
++import type { ReactNode } from 'react';
++
++import type { ModifierConfig, ViewEvent } from '../../types';
++import type { ButtonColors } from '../Button';
++import { createViewModifierEventListener } from '../modifiers/utils';
++
++export interface SplitButtonLayoutProps {
++ /**
++ * A `SplitButtonLayout.LeadingButton` and a `SplitButtonLayout.TrailingButton`.
++ */
++ children: ReactNode;
++ /**
++ * Modifiers for the component.
++ */
++ modifiers?: ModifierConfig[];
++}
++
++export interface SplitButtonLeadingButtonProps {
++ /**
++ * Callback that is called when the leading button is clicked.
++ */
++ onClick?: () => void;
++ /**
++ * Whether the button is enabled for user interaction.
++ * @default true
++ */
++ enabled?: boolean;
++ /**
++ * Colors for button elements.
++ */
++ colors?: ButtonColors;
++ /**
++ * Modifiers for the component.
++ */
++ modifiers?: ModifierConfig[];
++ /**
++ * Content to display inside the button.
++ */
++ children: ReactNode;
++}
++
++export interface SplitButtonTrailingButtonProps {
++ /**
++ * Whether the trailing button is checked, usually while its menu is expanded.
++ */
++ checked: boolean;
++ /**
++ * Callback that is called when the checked state changes.
++ */
++ onCheckedChange?: (checked: boolean) => void;
++ /**
++ * Whether the button is enabled for user interaction.
++ * @default true
++ */
++ enabled?: boolean;
++ /**
++ * Colors for button elements.
++ */
++ colors?: ButtonColors;
++ /**
++ * Modifiers for the component.
++ */
++ modifiers?: ModifierConfig[];
++ /**
++ * Content to display inside the button, usually a chevron icon.
++ */
++ children: ReactNode;
++}
++
++type NativeLeadingButtonProps = Omit &
++ ViewEvent<'onButtonPressed', void>;
++
++type NativeTrailingButtonProps = Omit &
++ ViewEvent<'onCheckedChange', { checked: boolean }>;
++
++const SplitButtonLayoutNativeView: React.ComponentType = requireNativeView(
++ 'ExpoUI',
++ 'SplitButtonLayoutView'
++);
++const LeadingButtonNativeView: React.ComponentType = requireNativeView(
++ 'ExpoUI',
++ 'SplitButtonLeadingButtonView'
++);
++const TrailingButtonNativeView: React.ComponentType = requireNativeView(
++ 'ExpoUI',
++ 'SplitButtonTrailingButtonView'
++);
++const SlotNativeView: React.ComponentType<{ slotName: string; children: ReactNode }> =
++ requireNativeView('ExpoUI', 'SlotView');
++
++function LeadingButton(props: SplitButtonLeadingButtonProps) {
++ const { onClick, modifiers, ...restProps } = props;
++ return (
++
++ onClick() : undefined}
++ />
++
++ );
++}
++
++function TrailingButton(props: SplitButtonTrailingButtonProps) {
++ const { onCheckedChange, modifiers, ...restProps } = props;
++ return (
++
++ onCheckedChange(checked) : undefined
++ }
++ />
++
++ );
++}
++
++/**
++ * A Material 3 split button: a leading action button and a trailing toggle button,
++ * usually anchoring a `DropdownMenu` of related actions.
++ */
++function SplitButtonLayout(props: SplitButtonLayoutProps) {
++ const { modifiers, ...restProps } = props;
++ return (
++
++ );
++}
++
++SplitButtonLayout.LeadingButton = LeadingButton;
++SplitButtonLayout.TrailingButton = TrailingButton;
++
++export { SplitButtonLayout };
diff --git a/src/jetpack-compose/TopAppBar/index.tsx b/src/jetpack-compose/TopAppBar/index.tsx
new file mode 100644
-index 0000000..ca69824
+index 0000000000000000000000000000000000000000..ca6982443d5711c48e90e79a57b794055efbfcb5
--- /dev/null
+++ b/src/jetpack-compose/TopAppBar/index.tsx
@@ -0,0 +1,28 @@
@@ -335,7 +773,7 @@ index 0000000..ca69824
+TopAppBar.NavigationIcon = function NavigationIcon({ children }: { children: ReactNode }) { return {children}; };
+TopAppBar.Actions = function Actions({ children }: { children: ReactNode }) { return {children}; };
diff --git a/src/jetpack-compose/index.ts b/src/jetpack-compose/index.ts
-index 77fe3d7..3fa0fb9 100644
+index 77fe3d7412fb0ef423e3d7a870676ebb54f07350..08ab59f63c7b40f4727500a37d95bed4f262de82 100644
--- a/src/jetpack-compose/index.ts
+++ b/src/jetpack-compose/index.ts
@@ -1,3 +1,4 @@
@@ -343,3 +781,11 @@ index 77fe3d7..3fa0fb9 100644
import './MaterialSymbolsAssetsTransformer.fx';
export * from './AlertDialog';
+@@ -22,6 +23,7 @@ export * from './ListItem';
+ export * from './RNHostView';
+ export * from './DatePicker';
+ export * from './SegmentedButton';
++export * from './SplitButton';
+ export * from './Progress';
+ export * from './Slider';
+ export * from './Spacer';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6bdece6bd1839..f2eabadf0a4f9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -361,7 +361,7 @@ patchedDependencies:
'@effect/vitest@4.0.5': 2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02
'@expo/fingerprint@0.21.5': 8fba80d6bbce46c72de427fe8c7907462d262a0bfe50c6b2b7ee9c4e0c61e5e1
'@expo/metro-config@58.0.14': 90fa980e8b6480631fb2fbb04559834e3c612db40c550cd828f11be297d8d760
- '@expo/ui@58.0.20': 5a26480373850c1a7943919aa21f904151b335a0cb01042109e0746def37923e
+ '@expo/ui@58.0.20': 64559cb60723e19a8a72256d4c78d1315d937a7af3b32d19860ea596f88736ce
'@ff-labs/fff-node@0.11.0': cc71fcec015f4d39bc96ae2e2bf6b0ca678124c722c66a7841efce9d34b5983c
'@legendapp/list@3.3.5': 6cbdacd8c380d9a5afb9f73d4198dc2cc44be6ef28a955b1183c781469307896
'@muse-code/sdk@1.4.6': 7b495b607fc5d76b444c7c4d58821e839726a30e6f3c53564f0d59f2d5b42101
@@ -538,7 +538,7 @@ importers:
version: 58.0.14(@expo/log-box@58.0.12)(expo@58.0.14)(react-dom@19.3.0(react@19.3.0))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)
'@expo/ui':
specifier: 58.0.20
- version: 58.0.20(patch_hash=5a26480373850c1a7943919aa21f904151b335a0cb01042109e0746def37923e)(@babel/core@7.29.7(supports-color@8.1.1))(expo@58.0.14)(react-dom@19.3.0(react@19.3.0))(react-native-worklets@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)(supports-color@8.1.1))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)
+ version: 58.0.20(patch_hash=64559cb60723e19a8a72256d4c78d1315d937a7af3b32d19860ea596f88736ce)(@babel/core@7.29.7(supports-color@8.1.1))(expo@58.0.14)(react-dom@19.3.0(react@19.3.0))(react-native-worklets@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)(supports-color@8.1.1))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)
'@legendapp/list':
specifier: 3.3.5
version: 3.3.5(patch_hash=6cbdacd8c380d9a5afb9f73d4198dc2cc44be6ef28a955b1183c781469307896)(react-dom@19.3.0(react@19.3.0))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)
@@ -14342,7 +14342,7 @@ snapshots:
'@expo/sudo-prompt@9.3.2': {}
- ? '@expo/ui@58.0.20(patch_hash=5a26480373850c1a7943919aa21f904151b335a0cb01042109e0746def37923e)(@babel/core@7.29.7(supports-color@8.1.1))(expo@58.0.14)(react-dom@19.3.0(react@19.3.0))(react-native-worklets@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)(supports-color@8.1.1))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)'
+ ? '@expo/ui@58.0.20(patch_hash=64559cb60723e19a8a72256d4c78d1315d937a7af3b32d19860ea596f88736ce)(@babel/core@7.29.7(supports-color@8.1.1))(expo@58.0.14)(react-dom@19.3.0(react@19.3.0))(react-native-worklets@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)(supports-color@8.1.1))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)'
: dependencies:
expo: 58.0.14(277a83826bb26ba15256633fefeb0bc2)
react: 19.3.0
@@ -18593,7 +18593,7 @@ snapshots:
? expo-widgets@58.0.20(patch_hash=8bf49067f93742855e15f29559ac75c8b50230b47069060993c9ea01dac11171)(@babel/core@7.29.7(supports-color@8.1.1))(expo@58.0.14)(react-dom@19.3.0(react@19.3.0))(react-native-worklets@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)(supports-color@8.1.1))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)
: dependencies:
'@expo/plist': 0.10.1
- '@expo/ui': 58.0.20(patch_hash=5a26480373850c1a7943919aa21f904151b335a0cb01042109e0746def37923e)(@babel/core@7.29.7(supports-color@8.1.1))(expo@58.0.14)(react-dom@19.3.0(react@19.3.0))(react-native-worklets@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)(supports-color@8.1.1))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)
+ '@expo/ui': 58.0.20(patch_hash=64559cb60723e19a8a72256d4c78d1315d937a7af3b32d19860ea596f88736ce)(@babel/core@7.29.7(supports-color@8.1.1))(expo@58.0.14)(react-dom@19.3.0(react@19.3.0))(react-native-worklets@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)(supports-color@8.1.1))(react-native@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(react@19.3.0)
expo: 58.0.14(277a83826bb26ba15256633fefeb0bc2)
react: 19.3.0
react-native: 0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.88.0(@babel/core@7.29.7(supports-color@8.1.1))(bufferutil@4.1.0)(supports-color@8.1.1)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(supports-color@8.1.1)(utf-8-validate@6.0.6)