diff --git a/apps/mobile/src/components/ListRow.tsx b/apps/mobile/src/components/ListRow.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eeacede11f92e5f4d3fdb8336b9ef4711178571b
--- /dev/null
+++ b/apps/mobile/src/components/ListRow.tsx
@@ -0,0 +1,36 @@
+import { Platform } from "react-native";
+
+import { useResolveClassNames } from "uniwind";
+import { SymbolView } from "./AppSymbol";
+import { ListRowLayout, type ListRowContentProps } from "./ListRowLayout";
+import { RowPressable } from "./RowPressable";
+
+/** A navigation row owns its look; callers supply content and destination behavior. */
+export function ListRow(props: ListRowContentProps & { readonly onPress: () => void }) {
+ const rippleStyle = useResolveClassNames("bg-subtle-strong");
+ return (
+
+
+ }
+ />
+
+ );
+}
diff --git a/apps/mobile/src/components/ListRowLayout.tsx b/apps/mobile/src/components/ListRowLayout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..96138fdbb8a009b9c3a49a483298a2b7a975b8ba
--- /dev/null
+++ b/apps/mobile/src/components/ListRowLayout.tsx
@@ -0,0 +1,68 @@
+import type { ReactNode } from "react";
+import { Platform, useWindowDimensions, View } from "react-native";
+
+import { cn } from "../lib/cn";
+import { AppText } from "./AppText";
+import { SymbolView, type AppSymbolName } from "./AppSymbol";
+
+export interface ListRowContentProps {
+ readonly icon?: AppSymbolName;
+ readonly label: string;
+ readonly description?: string;
+ readonly detail?: string;
+ readonly disabled?: boolean;
+}
+
+/** Shared row geometry; controls own their behavior and accessibility. */
+export function ListRowLayout({
+ icon,
+ label,
+ description,
+ detail,
+ disabled,
+ accessory,
+}: ListRowContentProps & { readonly accessory?: ReactNode }) {
+ const { fontScale } = useWindowDimensions();
+ // Leave the label room to wrap at accessibility text sizes.
+ const stackDetail = fontScale >= 1.5;
+ return (
+
+ {icon ? (
+
+
+
+ ) : null}
+
+ {label}
+ {description ? (
+ {description}
+ ) : null}
+ {detail && stackDetail ? (
+ {detail}
+ ) : null}
+
+ {detail && !stackDetail ? (
+
+ {detail}
+
+ ) : null}
+ {accessory}
+
+ );
+}
diff --git a/apps/mobile/src/components/ListSection.tsx b/apps/mobile/src/components/ListSection.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..99e07d05e9bb7365883c7352c7f1263b20b4690e
--- /dev/null
+++ b/apps/mobile/src/components/ListSection.tsx
@@ -0,0 +1,46 @@
+import type { ReactNode } from "react";
+import { Platform, View } from "react-native";
+
+import { AppText as Text } from "./AppText";
+
+export function ListSection(props: {
+ readonly title?: string;
+ readonly titleIcon?: ReactNode;
+ readonly trailing?: ReactNode;
+ readonly children: ReactNode;
+ readonly footer?: string;
+}) {
+ return (
+
+ {props.title ? (
+
+
+ {props.titleIcon}
+
+ {props.title}
+
+
+ {props.trailing}
+
+ ) : null}
+
+ {props.children}
+
+ {props.footer ? (
+ {props.footer}
+ ) : null}
+
+ );
+}
diff --git a/apps/mobile/src/features/settings/components/SettingsRow.tsx b/apps/mobile/src/features/settings/components/SettingsRow.tsx
index 53bf1b55b452c49c1280bd1aba144ccaaf11e185..457107ec113b02683a232329afc076527bcc3c17 100644
--- a/apps/mobile/src/features/settings/components/SettingsRow.tsx
+++ b/apps/mobile/src/features/settings/components/SettingsRow.tsx
@@ -1,18 +1,13 @@
-import { MaterialListRow } from "../../../components/MaterialListRow";
import { useNavigation } from "@react-navigation/native";
import type { ComponentProps } from "react";
-import { Platform, Pressable, View } from "react-native";
+import { Platform } from "react-native";
-import { SymbolView } from "../../../components/AppSymbol";
-import { AppText as Text } from "../../../components/AppText";
+import { ListRow } from "../../../components/ListRow";
import type { SettingsLegalDocumentTarget, SettingsSheetTarget } from "./settings-sheet-targets";
-import { cn } from "../../../lib/cn";
-
-type SymbolName = ComponentProps["name"];
export function SettingsRow(props: {
readonly disabled?: boolean;
- readonly icon: SymbolName;
+ readonly icon: ComponentProps["icon"];
readonly label: string;
readonly value?: string;
readonly valuePosition?: "below" | "trailing";
@@ -21,119 +16,28 @@ export function SettingsRow(props: {
readonly onPress?: () => void;
}) {
const navigation = useNavigation();
- if (Platform.OS === "android") {
- return (
-
- {props.value}
-
-
- ) : undefined
- }
- disabled={props.disabled}
- leading={
-
- }
- onPress={() => {
- if (props.target)
- navigation.navigate("SettingsSheet", {
- screen: "SettingsContent",
- params: { screen: props.target },
- });
- else if (props.fullScreenTarget) navigation.navigate(props.fullScreenTarget);
- else props.onPress?.();
- }}
- />
- );
- }
- const content = (
-
-
- <>
-
- {props.label}
-
-
- {props.value ? (
-
- {props.value}
-
- ) : null}
-
- >
-
-
- );
-
- const target = props.target;
- if (target) {
- return (
-
- navigation.navigate("SettingsSheet", {
- screen: "SettingsContent",
- params: { screen: target },
- })
- }
- >
- {content}
-
- );
- }
-
- const fullScreenTarget = props.fullScreenTarget;
- if (fullScreenTarget) {
- return (
- navigation.navigate(fullScreenTarget)}
- >
- {content}
-
- );
- }
-
+ const valueBelow =
+ props.valuePosition === "below" ||
+ (props.valuePosition === undefined && Platform.OS === "android");
return (
-
- {content}
-
+ onPress={() => {
+ if (props.target) {
+ navigation.navigate("SettingsSheet", {
+ screen: "SettingsContent",
+ params: { screen: props.target },
+ });
+ } else if (props.fullScreenTarget) {
+ navigation.navigate(props.fullScreenTarget);
+ } else {
+ props.onPress?.();
+ }
+ }}
+ />
);
}
diff --git a/apps/mobile/src/features/settings/components/SettingsSection.tsx b/apps/mobile/src/features/settings/components/SettingsSection.tsx
index 68cf53f07788cb630e8c61703e4d649a85e87026..91d1f0138d43545da40368f6b03c731869591190 100644
--- a/apps/mobile/src/features/settings/components/SettingsSection.tsx
+++ b/apps/mobile/src/features/settings/components/SettingsSection.tsx
@@ -1,48 +1 @@
-import type { ReactNode } from "react";
-import { Platform, View } from "react-native";
-
-import { AppText as Text } from "../../../components/AppText";
-
-export function SettingsSection(props: {
- readonly title?: string;
- readonly titleIcon?: ReactNode;
- readonly trailing?: ReactNode;
- readonly children: ReactNode;
-}) {
- return (
-
- {props.title ? (
-
-
- {props.titleIcon}
-
- {props.title}
-
-
- {props.trailing}
-
- ) : null}
-
- {props.children}
-
-
- );
-}
+export { ListSection as SettingsSection } from "../../../components/ListSection";