| author | |
| committer | |
| log | ea15f17afd94e7ce7133e52f5b5983910662c9ec |
| tree | 03e4ecff0e222eee1fd24861c3c9a46f9352bfee |
| parent | 3bb06ad910389fcfc886bd5156f41dc73f307f80 |
| signature | Signed by PGP key B5690EEEBB952194 |
10 files changed, 284 insertions(+), 244 deletions(-)
apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx+1-1| ... | @@ -26,7 +26,7 @@ export function CodeAppearanceSection() { | ... | @@ -26,7 +26,7 @@ export function CodeAppearanceSection() { |
| 26 | ); | 26 | ); |
| 27 | 27 | ||
| 28 | return ( | 28 | return ( |
| 29 | <SettingsSection card title="Code & Diffs"> | 29 | <SettingsSection title="Code & Diffs"> |
| 30 | <CodeAppearancePreview | 30 | <CodeAppearancePreview |
| 31 | fontSize={appearance.codeFontSize} | 31 | fontSize={appearance.codeFontSize} |
| 32 | wordBreak={appearance.codeWordBreak} | 32 | wordBreak={appearance.codeWordBreak} |
apps/mobile/src/features/settings/appearance/sections/TerminalAppearanceSection.tsx+1-1| ... | @@ -26,7 +26,7 @@ export function TerminalAppearanceSection() { | ... | @@ -26,7 +26,7 @@ export function TerminalAppearanceSection() { |
| 26 | ); | 26 | ); |
| 27 | 27 | ||
| 28 | return ( | 28 | return ( |
| 29 | <SettingsSection card title="Terminal"> | 29 | <SettingsSection title="Terminal"> |
| 30 | <TerminalAppearancePreview fontSize={appearance.terminalFontSize} /> | 30 | <TerminalAppearancePreview fontSize={appearance.terminalFontSize} /> |
| 31 | <AppearancePreviewSeparator /> | 31 | <AppearancePreviewSeparator /> |
| 32 | <SettingsSwitchRow | 32 | <SettingsSwitchRow |
apps/mobile/src/features/settings/appearance/sections/TextAppearanceSection.tsx+1-1| ... | @@ -15,7 +15,7 @@ export function TextAppearanceSection() { | ... | @@ -15,7 +15,7 @@ export function TextAppearanceSection() { |
| 15 | const { isReady, appearance, setBaseFontSize } = useAppearancePreferences(); | 15 | const { isReady, appearance, setBaseFontSize } = useAppearancePreferences(); |
| 16 | 16 | ||
| 17 | return ( | 17 | return ( |
| 18 | <SettingsSection card title="Text"> | 18 | <SettingsSection title="Text"> |
| 19 | <TextAppearancePreview fontSize={appearance.baseFontSize} /> | 19 | <TextAppearancePreview fontSize={appearance.baseFontSize} /> |
| 20 | <AppearancePreviewSeparator /> | 20 | <AppearancePreviewSeparator /> |
| 21 | <FontSizeSliderRow | 21 | <FontSizeSliderRow |
apps/mobile/src/features/settings/components/SettingsSection.tsx+1-5| ... | @@ -7,8 +7,6 @@ export function SettingsSection(props: { | ... | @@ -7,8 +7,6 @@ export function SettingsSection(props: { |
| 7 | readonly title?: string; | 7 | readonly title?: string; |
| 8 | readonly trailing?: ReactNode; | 8 | readonly trailing?: ReactNode; |
| 9 | readonly children: ReactNode; | 9 | readonly children: ReactNode; |
| 10 | /** Force the grouped card background; Android otherwise lists options flat. */ | ||
| 11 | readonly card?: boolean; | ||
| 12 | }) { | 10 | }) { |
| 13 | return ( | 11 | return ( |
| 14 | <View className="gap-2"> | 12 | <View className="gap-2"> |
| ... | @@ -30,9 +28,7 @@ export function SettingsSection(props: { | ... | @@ -30,9 +28,7 @@ export function SettingsSection(props: { |
| 30 | className={ | 28 | className={ |
| 31 | Platform.OS === "android" | 29 | Platform.OS === "android" |
| 32 | ? "overflow-hidden rounded-[28px] bg-card" | 30 | ? "overflow-hidden rounded-[28px] bg-card" |
| 33 | : props.card | 31 | : "overflow-hidden rounded-[24px] border-continuous bg-card" |
| 34 | ? "overflow-hidden rounded-[24px] border-continuous bg-card" | ||
| 35 | : "overflow-hidden rounded-[24px] border-continuous bg-card android:bg-transparent" | ||
| 36 | } | 32 | } |
| 37 | > | 33 | > |
| 38 | {props.children} | 34 | {props.children} |
apps/mobile/src/features/threads/CustomSnoozeSheet.android.tsx+4-8| ... | @@ -1,7 +1,6 @@ | ... | @@ -1,7 +1,6 @@ |
| 1 | import { MaterialSegmentedButtons } from "../../components/MaterialSegmentedButtons.android"; | 1 | import { MaterialSegmentedButtons } from "../../components/MaterialSegmentedButtons.android"; |
| 2 | import { | 2 | import { |
| 3 | BasicAlertDialog, | 3 | BasicAlertDialog, |
| 4 | Button, | ||
| 5 | Column, | 4 | Column, |
| 6 | DateTimePicker, | 5 | DateTimePicker, |
| 7 | Host, | 6 | Host, |
| ... | @@ -240,19 +239,16 @@ export function CustomSnoozeSheet(props: Props) { | ... | @@ -240,19 +239,16 @@ export function CustomSnoozeSheet(props: Props) { |
| 240 | <Row horizontalArrangement="end" modifiers={[fillMaxWidth()]}> | 239 | <Row horizontalArrangement="end" modifiers={[fillMaxWidth()]}> |
| 241 | <TextButton | 240 | <TextButton |
| 242 | onClick={props.onClose} | 241 | onClick={props.onClose} |
| 243 | colors={{ contentColor: colors["--color-primary"] }} | 242 | colors={{ contentColor: colors["--color-foreground"] }} |
| 244 | > | 243 | > |
| 245 | <Text style={bodyTypography}>Cancel</Text> | 244 | <Text style={bodyTypography}>Cancel</Text> |
| 246 | </TextButton> | 245 | </TextButton> |
| 247 | <Button | 246 | <TextButton |
| 248 | onClick={submit} | 247 | onClick={submit} |
| 249 | colors={{ | 248 | colors={{ contentColor: colors["--color-foreground"] }} |
| 250 | containerColor: colors["--color-primary"], | ||
| 251 | contentColor: colors["--color-primary-foreground"], | ||
| 252 | }} | ||
| 253 | > | 249 | > |
| 254 | <Text style={bodyTypography}>Snooze</Text> | 250 | <Text style={bodyTypography}>Snooze</Text> |
| 255 | </Button> | 251 | </TextButton> |
| 256 | </Row> | 252 | </Row> |
| 257 | </Column> | 253 | </Column> |
| 258 | </Column> | 254 | </Column> |
apps/mobile/src/features/threads/CustomSnoozeSheet.ios.tsx+172-124| ... | @@ -1,27 +1,22 @@ | ... | @@ -1,27 +1,22 @@ |
| 1 | import { | 1 | import { |
| 2 | Button, | ||
| 3 | DatePicker, | 2 | DatePicker, |
| 4 | Host, | 3 | Host, |
| 5 | HStack, | 4 | HStack, |
| 6 | Picker, | 5 | Picker, |
| 7 | Popover, | 6 | Popover, |
| 7 | RNHostView, | ||
| 8 | Spacer, | 8 | Spacer, |
| 9 | Text, | 9 | Text, |
| 10 | VStack, | 10 | VStack, |
| 11 | } from "@expo/ui/swift-ui"; | 11 | } from "@expo/ui/swift-ui"; |
| 12 | import { | 12 | import { |
| 13 | accessibilityAddTraits, | ||
| 14 | accessibilityHidden, | 13 | accessibilityHidden, |
| 15 | buttonBorderShape, | ||
| 16 | buttonStyle, | ||
| 17 | clipped, | 14 | clipped, |
| 18 | controlSize, | ||
| 19 | font, | 15 | font, |
| 20 | datePickerStyle, | 16 | datePickerStyle, |
| 21 | foregroundStyle, | 17 | foregroundStyle, |
| 22 | frame, | 18 | frame, |
| 23 | labelsHidden, | 19 | labelsHidden, |
| 24 | labelStyle, | ||
| 25 | padding, | 20 | padding, |
| 26 | pickerStyle, | 21 | pickerStyle, |
| 27 | tag, | 22 | tag, |
| ... | @@ -32,12 +27,17 @@ import { | ... | @@ -32,12 +27,17 @@ import { |
| 32 | resolveCustomSnooze, | 27 | resolveCustomSnooze, |
| 33 | type CustomSnoozeInput, | 28 | type CustomSnoozeInput, |
| 34 | } from "@t3tools/client-runtime/state/thread-settled"; | 29 | } from "@t3tools/client-runtime/state/thread-settled"; |
| 35 | import { useState } from "react"; | 30 | import { useState, type ReactNode } from "react"; |
| 36 | import { useWindowDimensions } from "react-native"; | 31 | import { NavigationContainer, NavigationIndependentTree } from "@react-navigation/native"; |
| 32 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; | ||
| 33 | import { ScrollView, useWindowDimensions, View } from "react-native"; | ||
| 34 | import { useMobileNavigationTheme } from "../../lib/useMobileNavigationTheme"; | ||
| 35 | import { NativeHeaderToolbar } from "../../native/StackHeader"; | ||
| 37 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | 36 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 38 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | 37 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| 39 | 38 | ||
| 40 | const durationAmounts = Array.from({ length: 99 }, (_, index) => index + 1); | 39 | const durationAmounts = Array.from({ length: 99 }, (_, index) => index + 1); |
| 40 | const SnoozeStack = createNativeStackNavigator<{ CustomSnooze: undefined }>(); | ||
| 41 | const modes = [ | 41 | const modes = [ |
| 42 | { value: "date", label: "Date and time" }, | 42 | { value: "date", label: "Date and time" }, |
| 43 | { value: "duration", label: "Duration" }, | 43 | { value: "duration", label: "Duration" }, |
| ... | @@ -52,13 +52,15 @@ export function CustomSnoozeSheet(props: { | ... | @@ -52,13 +52,15 @@ export function CustomSnoozeSheet(props: { |
| 52 | readonly onClose: () => void; | 52 | readonly onClose: () => void; |
| 53 | readonly onSnooze: (snoozedUntil: string) => void; | 53 | readonly onSnooze: (snoozedUntil: string) => void; |
| 54 | }) { | 54 | }) { |
| 55 | const { width } = useWindowDimensions(); | 55 | const { width, height } = useWindowDimensions(); |
| 56 | const [mode, setMode] = useState<CustomSnoozeInput["mode"]>("date"); | 56 | const [mode, setMode] = useState<CustomSnoozeInput["mode"]>("date"); |
| 57 | const [date, setDate] = useState(() => new Date(Date.now() + 3_600_000)); | 57 | const [date, setDate] = useState(() => new Date(Date.now() + 3_600_000)); |
| 58 | const [amount, setAmount] = useState(2); | 58 | const [amount, setAmount] = useState(2); |
| 59 | const [unit, setUnit] = useState<"minutes" | "hours" | "days">("hours"); | 59 | const [unit, setUnit] = useState<"minutes" | "hours" | "days">("hours"); |
| 60 | const [error, setError] = useState<string | null>(null); | 60 | const [error, setError] = useState<string | null>(null); |
| 61 | const { themeVariables: colors, themeAppearance } = useAppearancePreferences(); | 61 | const { themeVariables: colors, themeAppearance } = useAppearancePreferences(); |
| 62 | const popoverWidth = Math.min(360, width - 32); | ||
| 63 | const popoverHeight = Math.min(error ? 364 : 324, height - 96); | ||
| 62 | const updateDate = (value: Date) => { | 64 | const updateDate = (value: Date) => { |
| 63 | setDate(value); | 65 | setDate(value); |
| 64 | setError(null); | 66 | setError(null); |
| ... | @@ -98,133 +100,179 @@ export function CustomSnoozeSheet(props: { | ... | @@ -98,133 +100,179 @@ export function CustomSnoozeSheet(props: { |
| 98 | /> | 100 | /> |
| 99 | </Popover.Trigger> | 101 | </Popover.Trigger> |
| 100 | <Popover.Content> | 102 | <Popover.Content> |
| 101 | <VStack | 103 | <SnoozePopoverNavigation |
| 102 | spacing={16} | 104 | width={popoverWidth} |
| 103 | modifiers={[padding({ all: 16 }), frame({ width: Math.min(360, width - 32) })]} | 105 | height={popoverHeight} |
| 106 | onClose={props.onClose} | ||
| 107 | onSubmit={submit} | ||
| 104 | > | 108 | > |
| 105 | <HStack spacing={12}> | 109 | <Host |
| 106 | <Button | 110 | matchContents={{ vertical: true }} |
| 107 | label="Cancel" | 111 | colorScheme={themeAppearance} |
| 108 | systemImage="xmark" | 112 | style={{ width: popoverWidth }} |
| 109 | role="cancel" | ||
| 110 | onPress={props.onClose} | ||
| 111 | modifiers={[ | ||
| 112 | labelStyle("iconOnly"), | ||
| 113 | buttonStyle(NATIVE_LIQUID_GLASS_SUPPORTED ? "glass" : "bordered"), | ||
| 114 | buttonBorderShape("circle"), | ||
| 115 | controlSize("large"), | ||
| 116 | ]} | ||
| 117 | /> | ||
| 118 | <Text | ||
| 119 | modifiers={[ | ||
| 120 | font({ textStyle: "headline" }), | ||
| 121 | frame({ maxWidth: Infinity, alignment: "leading" }), | ||
| 122 | accessibilityAddTraits(["isHeader"]), | ||
| 123 | ]} | ||
| 124 | > | ||
| 125 | Custom snooze | ||
| 126 | </Text> | ||
| 127 | <Button | ||
| 128 | label="Snooze" | ||
| 129 | onPress={submit} | ||
| 130 | modifiers={[ | ||
| 131 | buttonStyle( | ||
| 132 | NATIVE_LIQUID_GLASS_SUPPORTED ? "glassProminent" : "borderedProminent", | ||
| 133 | ), | ||
| 134 | controlSize("large"), | ||
| 135 | ]} | ||
| 136 | /> | ||
| 137 | </HStack> | ||
| 138 | <Picker | ||
| 139 | label="Snooze mode" | ||
| 140 | selection={mode} | ||
| 141 | onSelectionChange={(value: CustomSnoozeInput["mode"]) => { | ||
| 142 | setMode(value); | ||
| 143 | setError(null); | ||
| 144 | }} | ||
| 145 | modifiers={[pickerStyle("segmented")]} | ||
| 146 | > | 113 | > |
| 147 | {modes.map((option) => ( | 114 | <VStack |
| 148 | <Text key={option.value} modifiers={[tag(option.value)]}> | 115 | spacing={16} |
| 149 | {option.label} | 116 | modifiers={[padding({ all: 16 }), foregroundStyle(colors["--color-foreground"])]} |
| 150 | </Text> | 117 | > |
| 151 | ))} | ||
| 152 | </Picker> | ||
| 153 | {mode === "date" ? ( | ||
| 154 | <DatePicker | ||
| 155 | title="Snooze until" | ||
| 156 | selection={date} | ||
| 157 | displayedComponents={["date", "hourAndMinute"]} | ||
| 158 | onDateChange={updateDate} | ||
| 159 | modifiers={[ | ||
| 160 | datePickerStyle("wheel"), | ||
| 161 | labelsHidden(), | ||
| 162 | frame({ maxWidth: Infinity, height: 180 }), | ||
| 163 | ]} | ||
| 164 | /> | ||
| 165 | ) : ( | ||
| 166 | <HStack spacing={0}> | ||
| 167 | <Picker | ||
| 168 | label="Duration amount" | ||
| 169 | selection={amount} | ||
| 170 | onSelectionChange={(value: number) => { | ||
| 171 | setAmount(value); | ||
| 172 | setError(null); | ||
| 173 | }} | ||
| 174 | modifiers={[ | ||
| 175 | pickerStyle("wheel"), | ||
| 176 | labelsHidden(), | ||
| 177 | frame({ minWidth: 0, maxWidth: Infinity, height: 180 }), | ||
| 178 | clipped(), | ||
| 179 | ]} | ||
| 180 | > | ||
| 181 | {durationAmounts.map((value) => ( | ||
| 182 | <Text | ||
| 183 | key={value} | ||
| 184 | modifiers={[tag(value), foregroundStyle(colors["--color-foreground"])]} | ||
| 185 | > | ||
| 186 | {String(value)} | ||
| 187 | </Text> | ||
| 188 | ))} | ||
| 189 | </Picker> | ||
| 190 | <Picker | 118 | <Picker |
| 191 | label="Duration unit" | 119 | label="Snooze mode" |
| 192 | selection={unit} | 120 | selection={mode} |
| 193 | onSelectionChange={(value: typeof unit) => { | 121 | onSelectionChange={(value: CustomSnoozeInput["mode"]) => { |
| 194 | setUnit(value); | 122 | setMode(value); |
| 195 | setError(null); | 123 | setError(null); |
| 196 | }} | 124 | }} |
| 197 | modifiers={[ | 125 | modifiers={[pickerStyle("segmented")]} |
| 198 | pickerStyle("wheel"), | ||
| 199 | labelsHidden(), | ||
| 200 | frame({ minWidth: 0, maxWidth: Infinity, height: 180 }), | ||
| 201 | clipped(), | ||
| 202 | ]} | ||
| 203 | > | 126 | > |
| 204 | {units.map((option) => ( | 127 | {modes.map((option) => ( |
| 205 | <Text | 128 | <Text key={option.value} modifiers={[tag(option.value)]}> |
| 206 | key={option.value} | ||
| 207 | modifiers={[tag(option.value), foregroundStyle(colors["--color-foreground"])]} | ||
| 208 | > | ||
| 209 | {option.label} | 129 | {option.label} |
| 210 | </Text> | 130 | </Text> |
| 211 | ))} | 131 | ))} |
| 212 | </Picker> | 132 | </Picker> |
| 213 | </HStack> | 133 | {mode === "date" ? ( |
| 214 | )} | 134 | <DatePicker |
| 215 | {error ? ( | 135 | title="Snooze until" |
| 216 | <Text | 136 | selection={date} |
| 217 | modifiers={[ | 137 | displayedComponents={["date", "hourAndMinute"]} |
| 218 | font({ textStyle: "footnote" }), | 138 | onDateChange={updateDate} |
| 219 | foregroundStyle(colors["--color-danger-foreground"]), | 139 | modifiers={[ |
| 220 | ]} | 140 | datePickerStyle("wheel"), |
| 221 | > | 141 | labelsHidden(), |
| 222 | {error} | 142 | frame({ maxWidth: Infinity, height: 180 }), |
| 223 | </Text> | 143 | ]} |
| 224 | ) : null} | 144 | /> |
| 225 | </VStack> | 145 | ) : ( |
| 146 | <HStack spacing={0}> | ||
| 147 | <Picker | ||
| 148 | label="Duration amount" | ||
| 149 | selection={amount} | ||
| 150 | onSelectionChange={(value: number) => { | ||
| 151 | setAmount(value); | ||
| 152 | setError(null); | ||
| 153 | }} | ||
| 154 | modifiers={[ | ||
| 155 | pickerStyle("wheel"), | ||
| 156 | labelsHidden(), | ||
| 157 | frame({ minWidth: 0, maxWidth: Infinity, height: 180 }), | ||
| 158 | clipped(), | ||
| 159 | ]} | ||
| 160 | > | ||
| 161 | {durationAmounts.map((value) => ( | ||
| 162 | <Text | ||
| 163 | key={value} | ||
| 164 | modifiers={[tag(value), foregroundStyle(colors["--color-foreground"])]} | ||
| 165 | > | ||
| 166 | {String(value)} | ||
| 167 | </Text> | ||
| 168 | ))} | ||
| 169 | </Picker> | ||
| 170 | <Picker | ||
| 171 | label="Duration unit" | ||
| 172 | selection={unit} | ||
| 173 | onSelectionChange={(value: typeof unit) => { | ||
| 174 | setUnit(value); | ||
| 175 | setError(null); | ||
| 176 | }} | ||
| 177 | modifiers={[ | ||
| 178 | pickerStyle("wheel"), | ||
| 179 | labelsHidden(), | ||
| 180 | frame({ minWidth: 0, maxWidth: Infinity, height: 180 }), | ||
| 181 | clipped(), | ||
| 182 | ]} | ||
| 183 | > | ||
| 184 | {units.map((option) => ( | ||
| 185 | <Text | ||
| 186 | key={option.value} | ||
| 187 | modifiers={[ | ||
| 188 | tag(option.value), | ||
| 189 | foregroundStyle(colors["--color-foreground"]), | ||
| 190 | ]} | ||
| 191 | > | ||
| 192 | {option.label} | ||
| 193 | </Text> | ||
| 194 | ))} | ||
| 195 | </Picker> | ||
| 196 | </HStack> | ||
| 197 | )} | ||
| 198 | {error ? ( | ||
| 199 | <Text | ||
| 200 | modifiers={[ | ||
| 201 | font({ textStyle: "footnote" }), | ||
| 202 | foregroundStyle(colors["--color-danger-foreground"]), | ||
| 203 | ]} | ||
| 204 | > | ||
| 205 | {error} | ||
| 206 | </Text> | ||
| 207 | ) : null} | ||
| 208 | </VStack> | ||
| 209 | </Host> | ||
| 210 | </SnoozePopoverNavigation> | ||
| 226 | </Popover.Content> | 211 | </Popover.Content> |
| 227 | </Popover> | 212 | </Popover> |
| 228 | </Host> | 213 | </Host> |
| 229 | ); | 214 | ); |
| 230 | } | 215 | } |
| 216 | |||
| 217 | /** The popover owns a native navigation bar, just like the app's sheet screens. */ | ||
| 218 | function SnoozePopoverNavigation(props: { | ||
| 219 | readonly width: number; | ||
| 220 | readonly height: number; | ||
| 221 | readonly onClose: () => void; | ||
| 222 | readonly onSubmit: () => void; | ||
| 223 | readonly children: ReactNode; | ||
| 224 | }) { | ||
| 225 | const navigationTheme = useMobileNavigationTheme(); | ||
| 226 | const { themeVariables: colors } = useAppearancePreferences(); | ||
| 227 | |||
| 228 | return ( | ||
| 229 | <VStack modifiers={[frame({ width: props.width, height: props.height })]}> | ||
| 230 | <RNHostView> | ||
| 231 | <View style={{ width: props.width, height: props.height }}> | ||
| 232 | <NavigationIndependentTree> | ||
| 233 | <NavigationContainer theme={navigationTheme}> | ||
| 234 | <SnoozeStack.Navigator | ||
| 235 | screenOptions={{ | ||
| 236 | contentStyle: { backgroundColor: "transparent" }, | ||
| 237 | headerShadowVisible: false, | ||
| 238 | headerStyle: { | ||
| 239 | backgroundColor: NATIVE_LIQUID_GLASS_SUPPORTED | ||
| 240 | ? "transparent" | ||
| 241 | : colors["--color-card"], | ||
| 242 | }, | ||
| 243 | headerTintColor: colors["--color-foreground"], | ||
| 244 | headerTitleStyle: { fontSize: 17, fontWeight: "700" }, | ||
| 245 | headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED, | ||
| 246 | title: "Custom snooze", | ||
| 247 | }} | ||
| 248 | > | ||
| 249 | <SnoozeStack.Screen name="CustomSnooze"> | ||
| 250 | {() => ( | ||
| 251 | <> | ||
| 252 | <NativeHeaderToolbar placement="left"> | ||
| 253 | <NativeHeaderToolbar.Button | ||
| 254 | accessibilityLabel="Cancel custom snooze" | ||
| 255 | icon="xmark" | ||
| 256 | onPress={props.onClose} | ||
| 257 | /> | ||
| 258 | </NativeHeaderToolbar> | ||
| 259 | <NativeHeaderToolbar placement="right"> | ||
| 260 | <NativeHeaderToolbar.Button label="Snooze" onPress={props.onSubmit} /> | ||
| 261 | </NativeHeaderToolbar> | ||
| 262 | <ScrollView | ||
| 263 | contentInsetAdjustmentBehavior="automatic" | ||
| 264 | showsVerticalScrollIndicator={false} | ||
| 265 | > | ||
| 266 | {props.children} | ||
| 267 | </ScrollView> | ||
| 268 | </> | ||
| 269 | )} | ||
| 270 | </SnoozeStack.Screen> | ||
| 271 | </SnoozeStack.Navigator> | ||
| 272 | </NavigationContainer> | ||
| 273 | </NavigationIndependentTree> | ||
| 274 | </View> | ||
| 275 | </RNHostView> | ||
| 276 | </VStack> | ||
| 277 | ); | ||
| 278 | } |
apps/mobile/src/features/threads/CustomSnoozeSheet.shared.tsx+2-2| ... | @@ -152,7 +152,7 @@ export function CustomSnoozeSheet(props: { | ... | @@ -152,7 +152,7 @@ export function CustomSnoozeSheet(props: { |
| 152 | </Pressable> | 152 | </Pressable> |
| 153 | <Pressable | 153 | <Pressable |
| 154 | accessibilityRole="button" | 154 | accessibilityRole="button" |
| 155 | className="min-h-12 justify-center rounded-xl bg-primary px-3" | 155 | className="min-h-12 justify-center rounded-xl bg-subtle px-3" |
| 156 | onPress={() => { | 156 | onPress={() => { |
| 157 | const input: CustomSnoozeInput = | 157 | const input: CustomSnoozeInput = |
| 158 | mode === "date" | 158 | mode === "date" |
| ... | @@ -171,7 +171,7 @@ export function CustomSnoozeSheet(props: { | ... | @@ -171,7 +171,7 @@ export function CustomSnoozeSheet(props: { |
| 171 | props.onClose(); | 171 | props.onClose(); |
| 172 | }} | 172 | }} |
| 173 | > | 173 | > |
| 174 | <AppText className="text-primary-foreground">Snooze</AppText> | 174 | <AppText className="text-foreground">Snooze</AppText> |
| 175 | </Pressable> | 175 | </Pressable> |
| 176 | </View> | 176 | </View> |
| 177 | </ScrollView> | 177 | </ScrollView> |
apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+98-98| ... | @@ -9,7 +9,6 @@ import type { | ... | @@ -9,7 +9,6 @@ import type { |
| 9 | import type { LegendListRenderItemProps } from "@legendapp/list/react-native"; | 9 | import type { LegendListRenderItemProps } from "@legendapp/list/react-native"; |
| 10 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; | 10 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 11 | import { AnimatedLegendList } from "@legendapp/list/reanimated"; | 11 | import { AnimatedLegendList } from "@legendapp/list/reanimated"; |
| 12 | import { HeaderHeightContext } from "@react-navigation/elements"; | ||
| 13 | import { | 12 | import { |
| 14 | getProviderOptionCurrentLabel, | 13 | getProviderOptionCurrentLabel, |
| 15 | getProviderOptionCurrentValue, | 14 | getProviderOptionCurrentValue, |
| ... | @@ -31,13 +30,22 @@ import { | ... | @@ -31,13 +30,22 @@ import { |
| 31 | useState, | 30 | useState, |
| 32 | type ReactNode, | 31 | type ReactNode, |
| 33 | } from "react"; | 32 | } from "react"; |
| 34 | import { Alert, Platform, Pressable, ScrollView, TextInput, View } from "react-native"; | 33 | import { |
| 34 | Alert, | ||
| 35 | Platform, | ||
| 36 | Pressable, | ||
| 37 | RefreshControl, | ||
| 38 | ScrollView, | ||
| 39 | TextInput, | ||
| 40 | View, | ||
| 41 | } from "react-native"; | ||
| 35 | import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated"; | 42 | import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated"; |
| 36 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 43 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 37 | 44 | ||
| 38 | import { SymbolView } from "../../components/AppSymbol"; | 45 | import { SymbolView } from "../../components/AppSymbol"; |
| 39 | import { AppText as Text } from "../../components/AppText"; | 46 | import { AppText as Text } from "../../components/AppText"; |
| 40 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | 47 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 48 | import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu"; | ||
| 41 | import { MaterialButton } from "../../components/MaterialButton"; | 49 | import { MaterialButton } from "../../components/MaterialButton"; |
| 42 | import { MaterialIconButton } from "../../components/MaterialIconButton"; | 50 | import { MaterialIconButton } from "../../components/MaterialIconButton"; |
| 43 | import { ProviderIcon } from "../../components/ProviderIcon"; | 51 | import { ProviderIcon } from "../../components/ProviderIcon"; |
| ... | @@ -759,12 +767,27 @@ function ThreadSettingsMainContent(props: { | ... | @@ -759,12 +767,27 @@ function ThreadSettingsMainContent(props: { |
| 759 | readonly onOpenSubmenu: (submenu: ThreadSettingsSubmenuPage) => void; | 767 | readonly onOpenSubmenu: (submenu: ThreadSettingsSubmenuPage) => void; |
| 760 | }) { | 768 | }) { |
| 761 | const session = useThreadSettingsSession(); | 769 | const session = useThreadSettingsSession(); |
| 770 | const refreshProvidersCommand = useAtomCommand(serverEnvironment.refreshProviders, { | ||
| 771 | reportFailure: false, | ||
| 772 | }); | ||
| 773 | const refreshProviderCatalog = useMemo( | ||
| 774 | () => createProviderCatalogRefreshRunner(refreshProvidersCommand), | ||
| 775 | [refreshProvidersCommand], | ||
| 776 | ); | ||
| 777 | const [isRefreshingProviders, setIsRefreshingProviders] = useState(false); | ||
| 778 | const refreshProviders = useCallback(() => { | ||
| 779 | if (!session.environmentId || isRefreshingProviders) return; | ||
| 780 | setIsRefreshingProviders(true); | ||
| 781 | void refreshProviderCatalog(session.environmentId).then((result) => { | ||
| 782 | setIsRefreshingProviders(false); | ||
| 783 | const error = providerCatalogRefreshError(result); | ||
| 784 | if (error) Alert.alert("Could not refresh models", error); | ||
| 785 | }); | ||
| 786 | }, [isRefreshingProviders, refreshProviderCatalog, session.environmentId]); | ||
| 762 | const catalogItems = useThreadSettingsCatalogItems(session); | 787 | const catalogItems = useThreadSettingsCatalogItems(session); |
| 763 | const [animationsReady, setAnimationsReady] = useState(false); | 788 | const [animationsReady, setAnimationsReady] = useState(false); |
| 764 | const nativeHeaderHeight = use(HeaderHeightContext) ?? 0; | ||
| 765 | const hasActiveCatalogFilter = | 789 | const hasActiveCatalogFilter = |
| 766 | session.providerFilter !== null || session.searchQuery.trim().length > 0; | 790 | session.providerFilter !== null || session.searchQuery.trim().length > 0; |
| 767 | const usesTransparentNativeHeader = Platform.OS === "ios" && NATIVE_LIQUID_GLASS_SUPPORTED; | ||
| 768 | const listItems = useMemo<ReadonlyArray<ThreadSettingsCatalogItem>>( | 791 | const listItems = useMemo<ReadonlyArray<ThreadSettingsCatalogItem>>( |
| 769 | () => [ | 792 | () => [ |
| 770 | ...(catalogItems.length === 0 ? ([{ kind: "empty", key: "empty" }] as const) : catalogItems), | 793 | ...(catalogItems.length === 0 ? ([{ kind: "empty", key: "empty" }] as const) : catalogItems), |
| ... | @@ -830,6 +853,7 @@ function ThreadSettingsMainContent(props: { | ... | @@ -830,6 +853,7 @@ function ThreadSettingsMainContent(props: { |
| 830 | 853 | ||
| 831 | return ( | 854 | return ( |
| 832 | <AnimatedLegendList | 855 | <AnimatedLegendList |
| 856 | alwaysBounceVertical | ||
| 833 | automaticallyAdjustsScrollIndicatorInsets | 857 | automaticallyAdjustsScrollIndicatorInsets |
| 834 | className="flex-1 bg-sheet" | 858 | className="flex-1 bg-sheet" |
| 835 | style={ | 859 | style={ |
| ... | @@ -838,7 +862,7 @@ function ThreadSettingsMainContent(props: { | ... | @@ -838,7 +862,7 @@ function ThreadSettingsMainContent(props: { |
| 838 | : undefined | 862 | : undefined |
| 839 | } | 863 | } |
| 840 | contentContainerStyle={{ paddingTop: 4 }} | 864 | contentContainerStyle={{ paddingTop: 4 }} |
| 841 | contentInsetAdjustmentBehavior={usesTransparentNativeHeader ? "never" : "automatic"} | 865 | contentInsetAdjustmentBehavior="automatic" |
| 842 | data={listItems} | 866 | data={listItems} |
| 843 | estimatedItemSize={Platform.OS === "android" ? 56 : 48} | 867 | estimatedItemSize={Platform.OS === "android" ? 56 : 48} |
| 844 | extraData={animationsReady} | 868 | extraData={animationsReady} |
| ... | @@ -850,7 +874,6 @@ function ThreadSettingsMainContent(props: { | ... | @@ -850,7 +874,6 @@ function ThreadSettingsMainContent(props: { |
| 850 | maintainVisibleContentPosition={THREAD_SETTINGS_MAINTAIN_VISIBLE_CONTENT_POSITION} | 874 | maintainVisibleContentPosition={THREAD_SETTINGS_MAINTAIN_VISIBLE_CONTENT_POSITION} |
| 851 | ListHeaderComponent={ | 875 | ListHeaderComponent={ |
| 852 | <> | 876 | <> |
| 853 | {usesTransparentNativeHeader ? <View style={{ height: nativeHeaderHeight }} /> : null} | ||
| 854 | {Platform.OS === "android" ? ( | 877 | {Platform.OS === "android" ? ( |
| 855 | <View className="px-4 pb-2 pt-3"> | 878 | <View className="px-4 pb-2 pt-3"> |
| 856 | <View | 879 | <View |
| ... | @@ -886,43 +909,16 @@ function ThreadSettingsMainContent(props: { | ... | @@ -886,43 +909,16 @@ function ThreadSettingsMainContent(props: { |
| 886 | /> | 909 | /> |
| 887 | ) : null} | 910 | ) : null} |
| 888 | </View> | 911 | </View> |
| 889 | <View className="flex-row gap-2 pt-3"> | ||
| 890 | <Pressable | ||
| 891 | accessibilityRole="button" | ||
| 892 | accessibilityState={{ | ||
| 893 | selected: session.providerFilter !== FAVORITES_PROVIDER_FILTER, | ||
| 894 | }} | ||
| 895 | className={cn( | ||
| 896 | "rounded-full px-4 py-2", | ||
| 897 | session.providerFilter !== FAVORITES_PROVIDER_FILTER | ||
| 898 | ? "bg-subtle-strong" | ||
| 899 | : "bg-subtle", | ||
| 900 | )} | ||
| 901 | onPress={() => session.setProviderFilter(null)} | ||
| 902 | > | ||
| 903 | <Text className="text-sm font-t3-medium text-foreground">All</Text> | ||
| 904 | </Pressable> | ||
| 905 | <Pressable | ||
| 906 | accessibilityRole="button" | ||
| 907 | accessibilityState={{ | ||
| 908 | selected: session.providerFilter === FAVORITES_PROVIDER_FILTER, | ||
| 909 | }} | ||
| 910 | className={cn( | ||
| 911 | "rounded-full px-4 py-2", | ||
| 912 | session.providerFilter === FAVORITES_PROVIDER_FILTER | ||
| 913 | ? "bg-subtle-strong" | ||
| 914 | : "bg-subtle", | ||
| 915 | )} | ||
| 916 | onPress={() => session.setProviderFilter(FAVORITES_PROVIDER_FILTER)} | ||
| 917 | > | ||
| 918 | <Text className="text-sm font-t3-medium text-foreground">Favorites</Text> | ||
| 919 | </Pressable> | ||
| 920 | </View> | ||
| 921 | </View> | 912 | </View> |
| 922 | ) : null} | 913 | ) : null} |
| 923 | </> | 914 | </> |
| 924 | } | 915 | } |
| 925 | recycleItems | 916 | recycleItems |
| 917 | refreshControl={ | ||
| 918 | session.environmentId ? ( | ||
| 919 | <RefreshControl refreshing={isRefreshingProviders} onRefresh={refreshProviders} /> | ||
| 920 | ) : undefined | ||
| 921 | } | ||
| 926 | onLoad={() => setAnimationsReady(true)} | 922 | onLoad={() => setAnimationsReady(true)} |
| 927 | renderItem={renderCatalogItem} | 923 | renderItem={renderCatalogItem} |
| 928 | showsVerticalScrollIndicator={false} | 924 | showsVerticalScrollIndicator={false} |
| ... | @@ -1042,27 +1038,22 @@ function ThreadSettingsModelsScreen() { | ... | @@ -1042,27 +1038,22 @@ function ThreadSettingsModelsScreen() { |
| 1042 | const navigation = useNavigation<NativeStackNavigationProp<ThreadSettingsPickerStackParams>>(); | 1038 | const navigation = useNavigation<NativeStackNavigationProp<ThreadSettingsPickerStackParams>>(); |
| 1043 | const usesNativeMailSearchToolbar = Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; | 1039 | const usesNativeMailSearchToolbar = Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; |
| 1044 | const hasCustomCatalogFilter = session.providerFilter !== null || session.showLegacy; | 1040 | const hasCustomCatalogFilter = session.providerFilter !== null || session.showLegacy; |
| 1045 | const refreshProvidersCommand = useAtomCommand(serverEnvironment.refreshProviders, { | ||
| 1046 | reportFailure: false, | ||
| 1047 | }); | ||
| 1048 | const refreshProviderCatalog = useMemo( | ||
| 1049 | () => createProviderCatalogRefreshRunner(refreshProvidersCommand), | ||
| 1050 | [refreshProvidersCommand], | ||
| 1051 | ); | ||
| 1052 | const [isRefreshingProviders, setIsRefreshingProviders] = useState(false); | ||
| 1053 | const refreshProviders = useCallback(() => { | ||
| 1054 | if (!session.environmentId || isRefreshingProviders) return; | ||
| 1055 | setIsRefreshingProviders(true); | ||
| 1056 | void refreshProviderCatalog(session.environmentId).then((result) => { | ||
| 1057 | setIsRefreshingProviders(false); | ||
| 1058 | const error = providerCatalogRefreshError(result); | ||
| 1059 | if (error) Alert.alert("Could not refresh models", error); | ||
| 1060 | }); | ||
| 1061 | }, [isRefreshingProviders, refreshProviderCatalog, session.environmentId]); | ||
| 1062 | const commitAndClose = useCallback(() => { | 1041 | const commitAndClose = useCallback(() => { |
| 1063 | if (!session.commitPendingModel()) return; | 1042 | if (!session.commitPendingModel()) return; |
| 1064 | presentation.onClose(); | 1043 | presentation.onClose(); |
| 1065 | }, [presentation, session]); | 1044 | }, [presentation, session]); |
| 1045 | const providerFilters = useMemo( | ||
| 1046 | () => [ | ||
| 1047 | { id: "all-providers", title: "All providers", value: null }, | ||
| 1048 | { id: "favorites", title: "Favorites", value: FAVORITES_PROVIDER_FILTER }, | ||
| 1049 | ...session.providerGroups.map((group) => ({ | ||
| 1050 | id: `provider:${group.providerKey}`, | ||
| 1051 | title: group.providerLabel, | ||
| 1052 | value: group.providerKey, | ||
| 1053 | })), | ||
| 1054 | ], | ||
| 1055 | [session.providerGroups], | ||
| 1056 | ); | ||
| 1066 | const filterMenu = useMemo( | 1057 | const filterMenu = useMemo( |
| 1067 | () => ({ | 1058 | () => ({ |
| 1068 | title: "Model filters", | 1059 | title: "Model filters", |
| ... | @@ -1070,30 +1061,12 @@ function ThreadSettingsModelsScreen() { | ... | @@ -1070,30 +1061,12 @@ function ThreadSettingsModelsScreen() { |
| 1070 | { | 1061 | { |
| 1071 | type: "submenu" as const, | 1062 | type: "submenu" as const, |
| 1072 | title: "Provider", | 1063 | title: "Provider", |
| 1073 | items: [ | 1064 | items: providerFilters.map((filter) => ({ |
| 1074 | { | 1065 | type: "action" as const, |
| 1075 | type: "action" as const, | 1066 | title: filter.title, |
| 1076 | title: "All providers", | 1067 | state: session.providerFilter === filter.value ? ("on" as const) : ("off" as const), |
| 1077 | state: session.providerFilter === null ? ("on" as const) : ("off" as const), | 1068 | onPress: () => session.setProviderFilter(filter.value), |
| 1078 | onPress: () => session.setProviderFilter(null), | 1069 | })), |
| 1079 | }, | ||
| 1080 | { | ||
| 1081 | type: "action" as const, | ||
| 1082 | title: "Favorites", | ||
| 1083 | state: | ||
| 1084 | session.providerFilter === FAVORITES_PROVIDER_FILTER | ||
| 1085 | ? ("on" as const) | ||
| 1086 | : ("off" as const), | ||
| 1087 | onPress: () => session.setProviderFilter(FAVORITES_PROVIDER_FILTER), | ||
| 1088 | }, | ||
| 1089 | ...session.providerGroups.map((group) => ({ | ||
| 1090 | type: "action" as const, | ||
| 1091 | title: group.providerLabel, | ||
| 1092 | state: | ||
| 1093 | session.providerFilter === group.providerKey ? ("on" as const) : ("off" as const), | ||
| 1094 | onPress: () => session.setProviderFilter(group.providerKey), | ||
| 1095 | })), | ||
| 1096 | ], | ||
| 1097 | }, | 1070 | }, |
| 1098 | ...(session.hasLegacyModels | 1071 | ...(session.hasLegacyModels |
| 1099 | ? [ | 1072 | ? [ |
| ... | @@ -1107,25 +1080,59 @@ function ThreadSettingsModelsScreen() { | ... | @@ -1107,25 +1080,59 @@ function ThreadSettingsModelsScreen() { |
| 1107 | : []), | 1080 | : []), |
| 1108 | ], | 1081 | ], |
| 1109 | }), | 1082 | }), |
| 1110 | [session], | 1083 | [providerFilters, session], |
| 1111 | ); | 1084 | ); |
| 1112 | 1085 | ||
| 1113 | return ( | 1086 | return ( |
| 1114 | <> | 1087 | <> |
| 1115 | {Platform.OS === "android" ? ( | 1088 | {Platform.OS === "android" ? ( |
| 1116 | <AndroidScreenHeader | 1089 | <AndroidScreenHeader |
| 1117 | actions={[ | ||
| 1118 | { | ||
| 1119 | accessibilityLabel: "Refresh models", | ||
| 1120 | disabled: isRefreshingProviders || session.environmentId === null, | ||
| 1121 | icon: "arrow.clockwise", | ||
| 1122 | onPress: refreshProviders, | ||
| 1123 | }, | ||
| 1124 | ]} | ||
| 1125 | trailing={ | 1090 | trailing={ |
| 1126 | session.pendingModel ? ( | 1091 | <View className="flex-row items-center"> |
| 1127 | <MaterialButton label="Save" tone="text" onPress={commitAndClose} /> | 1092 | <AndroidAnchoredMenu |
| 1128 | ) : undefined | 1093 | title="Model filters" |
| 1094 | actions={[ | ||
| 1095 | { | ||
| 1096 | title: "Provider", | ||
| 1097 | subactions: providerFilters.map((filter) => ({ | ||
| 1098 | id: filter.id, | ||
| 1099 | title: filter.title, | ||
| 1100 | state: session.providerFilter === filter.value ? "on" : "off", | ||
| 1101 | })), | ||
| 1102 | }, | ||
| 1103 | ...(session.hasLegacyModels | ||
| 1104 | ? [ | ||
| 1105 | { | ||
| 1106 | id: "show-legacy", | ||
| 1107 | title: "Show legacy models", | ||
| 1108 | state: session.showLegacy ? ("on" as const) : ("off" as const), | ||
| 1109 | }, | ||
| 1110 | ] | ||
| 1111 | : []), | ||
| 1112 | ]} | ||
| 1113 | onPressAction={({ nativeEvent }) => { | ||
| 1114 | if (nativeEvent.event === "show-legacy") { | ||
| 1115 | session.setShowLegacy(!session.showLegacy); | ||
| 1116 | } else { | ||
| 1117 | const filter = providerFilters.find((item) => item.id === nativeEvent.event); | ||
| 1118 | if (filter) session.setProviderFilter(filter.value); | ||
| 1119 | } | ||
| 1120 | }} | ||
| 1121 | > | ||
| 1122 | {(open) => ( | ||
| 1123 | <MaterialIconButton | ||
| 1124 | accessibilityLabel="Filter models" | ||
| 1125 | icon="line.3.horizontal.decrease" | ||
| 1126 | selected={hasCustomCatalogFilter} | ||
| 1127 | variant={hasCustomCatalogFilter ? "tonal" : "standard"} | ||
| 1128 | onPress={open} | ||
| 1129 | /> | ||
| 1130 | )} | ||
| 1131 | </AndroidAnchoredMenu> | ||
| 1132 | {session.pendingModel ? ( | ||
| 1133 | <MaterialButton label="Save" tone="text" onPress={commitAndClose} /> | ||
| 1134 | ) : null} | ||
| 1135 | </View> | ||
| 1129 | } | 1136 | } |
| 1130 | onBack={presentation.onClose} | 1137 | onBack={presentation.onClose} |
| 1131 | title="Thread settings" | 1138 | title="Thread settings" |
| ... | @@ -1189,13 +1196,6 @@ function ThreadSettingsModelsScreen() { | ... | @@ -1189,13 +1196,6 @@ function ThreadSettingsModelsScreen() { |
| 1189 | /> | 1196 | /> |
| 1190 | </NativeHeaderToolbar> | 1197 | </NativeHeaderToolbar> |
| 1191 | <NativeHeaderToolbar placement="right"> | 1198 | <NativeHeaderToolbar placement="right"> |
| 1192 | <NativeHeaderToolbar.Button | ||
| 1193 | accessibilityLabel="Refresh models" | ||
| 1194 | disabled={isRefreshingProviders || session.environmentId === null} | ||
| 1195 | icon="arrow.clockwise" | ||
| 1196 | onPress={refreshProviders} | ||
| 1197 | separateBackground | ||
| 1198 | /> | ||
| 1199 | <NativeHeaderToolbar.Button | 1199 | <NativeHeaderToolbar.Button |
| 1200 | accessibilityLabel={session.pendingModel ? "Save thread settings" : "Done"} | 1200 | accessibilityLabel={session.pendingModel ? "Save thread settings" : "Done"} |
| 1201 | label={session.pendingModel ? "Save" : "Done"} | 1201 | label={session.pendingModel ? "Save" : "Done"} |
apps/mobile/src/features/threads/provider-catalog-refresh.ts+1-1| ... | @@ -10,7 +10,7 @@ type RefreshProvidersTarget = { | ... | @@ -10,7 +10,7 @@ type RefreshProvidersTarget = { |
| 10 | readonly input: { readonly refreshModels: true }; | 10 | readonly input: { readonly refreshModels: true }; |
| 11 | }; | 11 | }; |
| 12 | 12 | ||
| 13 | /** Explicit model-picker refresh. Repeated taps share the pending discovery. */ | 13 | /** Pull-to-refresh in the model picker shares any pending discovery. */ |
| 14 | export function createProviderCatalogRefreshRunner<Result>( | 14 | export function createProviderCatalogRefreshRunner<Result>( |
| 15 | refreshProviders: (target: RefreshProvidersTarget) => Promise<Result>, | 15 | refreshProviders: (target: RefreshProvidersTarget) => Promise<Result>, |
| 16 | ) { | 16 | ) { |
apps/mobile/src/features/usage/UsageRouteScreen.tsx+3-3| ... | @@ -412,7 +412,7 @@ function ProviderSection(props: { | ... | @@ -412,7 +412,7 @@ function ProviderSection(props: { |
| 412 | ); | 412 | ); |
| 413 | 413 | ||
| 414 | return ( | 414 | return ( |
| 415 | <SettingsSection title="Providers" card> | 415 | <SettingsSection title="Providers"> |
| 416 | {ordered.map((provider, index) => { | 416 | {ordered.map((provider, index) => { |
| 417 | const share = metric === "cost" ? provider.costShare : provider.tokenShare; | 417 | const share = metric === "cost" ? provider.costShare : provider.tokenShare; |
| 418 | return ( | 418 | return ( |
| ... | @@ -463,7 +463,7 @@ function TotalsSection(props: { readonly merged: MergedUsage; readonly isPast24H | ... | @@ -463,7 +463,7 @@ function TotalsSection(props: { readonly merged: MergedUsage; readonly isPast24H |
| 463 | const cachedShare = observedInput === 0 ? 0 : merged.cachedInputTokens / observedInput; | 463 | const cachedShare = observedInput === 0 ? 0 : merged.cachedInputTokens / observedInput; |
| 464 | 464 | ||
| 465 | return ( | 465 | return ( |
| 466 | <SettingsSection title="Totals" card> | 466 | <SettingsSection title="Totals"> |
| 467 | <View className="flex-row flex-wrap"> | 467 | <View className="flex-row flex-wrap"> |
| 468 | <MetricCell | 468 | <MetricCell |
| 469 | label="Processed tokens" | 469 | label="Processed tokens" |
| ... | @@ -524,7 +524,7 @@ function ModelsSection(props: { readonly merged: MergedUsage }) { | ... | @@ -524,7 +524,7 @@ function ModelsSection(props: { readonly merged: MergedUsage }) { |
| 524 | if (merged.models.length === 0) return null; | 524 | if (merged.models.length === 0) return null; |
| 525 | 525 | ||
| 526 | return ( | 526 | return ( |
| 527 | <SettingsSection title="By model" card> | 527 | <SettingsSection title="By model"> |
| 528 | {merged.models.map((model, index) => ( | 528 | {merged.models.map((model, index) => ( |
| 529 | <View | 529 | <View |
| 530 | key={`${model.provider}:${model.model}`} | 530 | key={`${model.provider}:${model.model}`} |