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";