diff --git a/app/account/tabs/dashboard-tab.tsx b/app/account/tabs/dashboard-tab.tsx index 2702e03..670650a 100644 --- a/app/account/tabs/dashboard-tab.tsx +++ b/app/account/tabs/dashboard-tab.tsx @@ -4,6 +4,7 @@ import { Pressable, StyleSheet, Text, + useWindowDimensions, View, type ImageSourcePropType, } from "react-native"; @@ -54,14 +55,20 @@ const CARDS: DashboardCard[] = [ }, ]; +const TABLET_BREAKPOINT = 768; +const TABLET_CONTENT_MAX_WIDTH = 760; +const TABLET_CARD_MIN_HEIGHT = 240; + export function DashboardTab({ accountId, handle, palette, onSelectTab, }: AccountTabProps) { + const { width } = useWindowDimensions(); const { state: cydState, checkPremiumAccess } = useCydAccount(); const [lastSavedAt, setLastSavedAt] = useState(null); + const isTablet = width >= TABLET_BREAKPOINT; // Load last saved timestamp useEffect(() => { @@ -96,53 +103,85 @@ export function DashboardTab({ }; return ( - - - - {CARDS.map((card) => { - const badge = getBadge(card.key); - return ( - onSelectTab?.(card.key)} - style={({ pressed }) => [ - styles.card, - { - borderColor: palette.icon + "22", - backgroundColor: palette.card, - opacity: pressed ? 0.92 : 1, - }, - ]} - > - {badge !== null && ( + + + + + {CARDS.map((card) => { + const badge = getBadge(card.key); + return ( + onSelectTab?.(card.key)} + style={({ pressed }) => [ + styles.card, + isTablet && styles.tabletCard, + { + borderColor: palette.icon + "22", + backgroundColor: palette.card, + opacity: pressed ? 0.92 : 1, + }, + ]} + > + {badge !== null && ( + + + {badge === "startHere" ? "Start Here" : "Premium"} + + + )} - - {badge === "startHere" ? "Start Here" : "Premium"} - + - )} - - - - - {card.title} - - - {card.description(handle)} - - - ); - })} + + {card.title} + + + {card.description(handle)} + + + ); + })} + ); @@ -152,6 +191,19 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + tabletContainer: { + width: "100%", + maxWidth: TABLET_CONTENT_MAX_WIDTH, + alignSelf: "center", + }, + contentStack: { + flex: 1, + }, + tabletContentStack: { + justifyContent: "center", + paddingTop: 20, + paddingBottom: 56, + }, grid: { flex: 1, flexDirection: "row", @@ -159,6 +211,11 @@ const styles = StyleSheet.create({ gap: 8, alignContent: "stretch", }, + tabletGrid: { + flex: 0, + alignContent: "flex-start", + gap: 12, + }, card: { borderRadius: 16, borderWidth: StyleSheet.hairlineWidth, @@ -171,6 +228,10 @@ const styles = StyleSheet.create({ position: "relative", overflow: "hidden", }, + tabletCard: { + minHeight: TABLET_CARD_MIN_HEIGHT, + padding: 22, + }, badge: { position: "absolute", top: 0, @@ -179,26 +240,46 @@ const styles = StyleSheet.create({ paddingVertical: 4, borderBottomLeftRadius: 10, }, + tabletBadge: { + paddingHorizontal: 10, + paddingVertical: 5, + }, badgeText: { color: "#ffffff", fontSize: 10, fontWeight: "700", textTransform: "uppercase", }, + tabletBadgeText: { + fontSize: 11, + }, iconContainer: { marginTop: 15, marginBottom: 8, }, + tabletIconContainer: { + marginTop: 22, + marginBottom: 14, + }, icon: { width: 60, height: 60, }, + tabletIcon: { + width: 72, + height: 72, + }, title: { fontSize: 15, fontWeight: "700", marginBottom: 4, textAlign: "center", }, + tabletTitle: { + fontSize: 18, + lineHeight: 22, + marginBottom: 8, + }, description: { fontSize: 15, lineHeight: 18, @@ -206,6 +287,10 @@ const styles = StyleSheet.create({ marginTop: "auto", marginBottom: "auto", }, + tabletDescription: { + fontSize: 17, + lineHeight: 24, + }, }); export default DashboardTab; diff --git a/app/index.tsx b/app/index.tsx index bc290b9..2d4a57b 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -9,6 +9,7 @@ import { RefreshControl, StyleSheet, Text, + useWindowDimensions, View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; @@ -21,13 +22,18 @@ import { useAccounts } from "@/hooks/use-accounts"; import { useColorScheme } from "@/hooks/use-color-scheme"; const CYD_DESKTOP_URL = "https://cyd.social/"; +const TABLET_BREAKPOINT = 768; +const TABLET_CONTENT_MAX_WIDTH = 400; +const TABLET_CTA_MAX_WIDTH = 520; export default function AccountSelectionScreen() { + const { width } = useWindowDimensions(); const colorScheme = useColorScheme(); const palette = getThemePalette(colorScheme); const Wordmark = colorScheme === "dark" ? WordmarkDark : WordmarkLight; const { accounts, loading, error, refresh } = useAccounts(); const router = useRouter(); + const isTablet = width >= TABLET_BREAKPOINT; const [navigatingAccountId, setNavigatingAccountId] = useState( null, ); @@ -114,79 +120,101 @@ export default function AccountSelectionScreen() { style={[styles.safeArea, { backgroundColor: palette.background }]} edges={["top", "left", "right"]} > - + {error ? ( Unable to load accounts. Pull to refresh. ) : null} - - - - + + + + + + - item.uuid} - renderItem={renderAccount} - ListEmptyComponent={listEmpty} - showsVerticalScrollIndicator={false} - contentContainerStyle={ - accounts.length === 0 ? styles.emptyListContainer : undefined - } - refreshControl={ - item.uuid} + renderItem={renderAccount} + ListEmptyComponent={listEmpty} + showsVerticalScrollIndicator={false} + style={styles.accountList} + contentContainerStyle={ + accounts.length === 0 ? styles.emptyListContainer : undefined + } + refreshControl={ + + } /> - } - /> + - [ - styles.addAccountButton, - { - backgroundColor: palette.button.background, - opacity: pressed ? 0.9 : 1, - }, - ]} - android_ripple={{ color: palette.button.ripple }} - > - [ + styles.addAccountButton, + isTablet && styles.tabletAddAccountButton, + { + backgroundColor: palette.button.background, + opacity: pressed ? 0.9 : 1, + }, ]} + android_ripple={{ color: palette.button.ripple }} > - Add Bluesky Account - - - + + Add Bluesky Account + + + - - Want to claw back your data from X (formerly Twitter)? - Use the{" "} - Cyd desktop app - {" "} - on a computer. - + Want to claw back your data from X (formerly Twitter)? Use the{" "} + + Cyd desktop app + {" "} + on a computer. + + ); @@ -285,13 +313,45 @@ const styles = StyleSheet.create({ paddingBottom: 24, gap: 16, }, + tabletContainer: { + alignItems: "center", + paddingTop: 48, + paddingBottom: 32, + }, + contentShell: { + flex: 1, + width: "100%", + gap: 16, + }, + tabletContentShell: { + maxWidth: TABLET_CONTENT_MAX_WIDTH, + }, mainContent: { flex: 1, gap: 20, }, + tabletMainContent: { + paddingTop: 16, + paddingBottom: 16, + }, + listSection: { + flex: 1, + minHeight: 0, + }, wordmarkWrapper: { - alignItems: "flex-start", + alignItems: "center", + alignSelf: "center", marginBottom: 20, + width: 160, + }, + tabletWordmarkWrapper: { + alignItems: "center", + alignSelf: "center", + width: 180, + marginBottom: 24, + }, + accountList: { + flex: 1, }, accountCard: { flexDirection: "row", @@ -338,6 +398,11 @@ const styles = StyleSheet.create({ paddingVertical: 16, alignItems: "center", }, + tabletAddAccountButton: { + width: "100%", + maxWidth: TABLET_CTA_MAX_WIDTH, + alignSelf: "center", + }, addAccountButtonText: { fontSize: 16, fontWeight: "600", @@ -347,6 +412,11 @@ const styles = StyleSheet.create({ lineHeight: 18, textAlign: "center", }, + tabletFooterText: { + width: "100%", + maxWidth: TABLET_CTA_MAX_WIDTH, + alignSelf: "center", + }, footerLink: { fontWeight: "600", }, @@ -364,6 +434,9 @@ const styles = StyleSheet.create({ paddingVertical: 10, paddingHorizontal: 16, }, + tabletBanner: { + width: "100%", + }, bannerText: { fontSize: 13, }, diff --git a/components/OnboardingModal.tsx b/components/OnboardingModal.tsx index f9d4a02..d46df82 100644 --- a/components/OnboardingModal.tsx +++ b/components/OnboardingModal.tsx @@ -7,6 +7,7 @@ import { ScrollView, StyleSheet, Text, + useWindowDimensions, View, } from "react-native"; import Markdown, { type MarkdownProps } from "react-native-markdown-display"; @@ -22,6 +23,10 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; const SCREEN_HEIGHT = Dimensions.get("window").height; const AVATAR_HEIGHT = Math.min(SCREEN_HEIGHT * 0.2, 280); +const TABLET_BREAKPOINT = 768; +const TABLET_CONTENT_MAX_WIDTH = 720; +const TABLET_BUTTONS_MAX_WIDTH = 520; +const TABLET_MODAL_VERTICAL_PADDING = 150; type OnboardingModalProps = { visible: boolean; @@ -71,9 +76,11 @@ If you want to delete your data on enshittified platforms like X (and, soon, Fac export function OnboardingModal({ visible, onClose }: OnboardingModalProps) { const insets = useSafeAreaInsets(); + const { width } = useWindowDimensions(); const colorScheme = useColorScheme(); const palette = getThemePalette(colorScheme); const [currentPage, setCurrentPage] = useState(0); + const isTablet = width >= TABLET_BREAKPOINT; const markdownStyles = useMemo( () => ({ @@ -142,83 +149,97 @@ export function OnboardingModal({ visible, onClose }: OnboardingModalProps) { styles.container, { backgroundColor: palette.background, - paddingTop: insets.top + 16, - paddingBottom: insets.bottom + 8, + paddingTop: insets.top + (isTablet ? 32 : 16), + paddingBottom: insets.bottom + (isTablet ? 24 : 8), }, ]} > - - - - - - { - void Linking.openURL(url); - return false; - }} + + + + + - {ONBOARDING_SCREENS[currentPage].content} - - - - - {!isFirstPage && ( + { + void Linking.openURL(url); + return false; + }} + > + {ONBOARDING_SCREENS[currentPage].content} + + + + + {!isFirstPage && ( + [ + styles.button, + styles.secondaryButton, + { + borderColor: palette.icon + "44", + opacity: pressed ? 0.8 : 1, + }, + ]} + accessibilityRole="button" + accessibilityLabel="Back" + > + + Back + + + )} [ styles.button, - styles.secondaryButton, + styles.primaryButton, { - borderColor: palette.icon + "44", - opacity: pressed ? 0.8 : 1, + backgroundColor: palette.button.background, + opacity: pressed ? 0.9 : 1, }, ]} accessibilityRole="button" - accessibilityLabel="Back" + accessibilityLabel={isLastPage ? "Finish" : "Continue"} > - - Back + + {isLastPage ? "Finish" : "Continue"} - )} - [ - styles.button, - styles.primaryButton, - { - backgroundColor: palette.button.background, - opacity: pressed ? 0.9 : 1, - }, - ]} - accessibilityRole="button" - accessibilityLabel={isLastPage ? "Finish" : "Continue"} - > - - {isLastPage ? "Finish" : "Continue"} - - - - - - {ONBOARDING_SCREENS.map((_, index) => ( - - ))} + + + + {ONBOARDING_SCREENS.map((_, index) => ( + + ))} + @@ -258,6 +279,16 @@ const styles = StyleSheet.create({ container: { flex: 1, paddingHorizontal: 24, + alignItems: "center", + }, + contentShell: { + flex: 1, + width: "100%", + }, + tabletContentShell: { + maxWidth: TABLET_CONTENT_MAX_WIDTH, + paddingTop: TABLET_MODAL_VERTICAL_PADDING, + paddingBottom: TABLET_MODAL_VERTICAL_PADDING, }, avatarContainer: { alignItems: "center", @@ -270,10 +301,18 @@ const styles = StyleSheet.create({ contentContainerInner: { flexGrow: 1, }, + tabletContentContainerInner: { + paddingVertical: 16, + }, buttonContainer: { flexDirection: "row", gap: 12, marginTop: 16, + width: "100%", + }, + tabletButtonContainer: { + maxWidth: TABLET_BUTTONS_MAX_WIDTH, + alignSelf: "center", }, button: { borderRadius: 16, diff --git a/components/cyd/SpeechBubble.tsx b/components/cyd/SpeechBubble.tsx index 102ebcc..888ed28 100644 --- a/components/cyd/SpeechBubble.tsx +++ b/components/cyd/SpeechBubble.tsx @@ -1,5 +1,10 @@ import { useMemo } from "react"; -import { Dimensions, StyleSheet, View } from "react-native"; +import { + Dimensions, + StyleSheet, + View, + useWindowDimensions, +} from "react-native"; import Markdown, { type MarkdownProps } from "react-native-markdown-display"; import { getThemePalette } from "@/constants/theme"; @@ -9,22 +14,30 @@ import { CydAvatar } from "./CydAvatar"; const SCREEN_HEIGHT = Dimensions.get("window").height; const TARGET_HEIGHT = Math.min(SCREEN_HEIGHT / 3, 260); const AVATAR_HEIGHT = Math.max(110, TARGET_HEIGHT * 0.65); +const TABLET_BREAKPOINT = 768; type SpeechBubbleProps = { message: string; avatarHeight?: number; + prominentOnTablet?: boolean; }; -export function SpeechBubble({ message, avatarHeight }: SpeechBubbleProps) { +export function SpeechBubble({ + message, + avatarHeight, + prominentOnTablet, +}: SpeechBubbleProps) { + const { width } = useWindowDimensions(); const colorScheme = useColorScheme(); const palette = getThemePalette(colorScheme); + const isProminentTablet = prominentOnTablet && width >= TABLET_BREAKPOINT; const bubbleBackground = colorScheme === "dark" ? "#202020ff" : "#f3f3f3"; const bubbleBorder = colorScheme === "dark" ? "#404040" : "#e0e0e0"; const markdownStyles = useMemo( () => ({ body: { - fontSize: 18, - lineHeight: 24, + fontSize: isProminentTablet ? 22 : 18, + lineHeight: isProminentTablet ? 30 : 24, color: palette.text, }, paragraph: { @@ -40,7 +53,7 @@ export function SpeechBubble({ message, avatarHeight }: SpeechBubbleProps) { color: palette.tint, }, }), - [palette.text, palette.tint], + [isProminentTablet, palette.text, palette.tint], ); const normalizedMessage = useMemo(() => { @@ -58,11 +71,18 @@ export function SpeechBubble({ message, avatarHeight }: SpeechBubbleProps) { }, [message]); return ( - - + +