From 0f91f47837fe76761aeacd67faf540a5329f26e0 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Wed, 10 Jun 2026 21:28:26 +0500 Subject: [PATCH] feat(web): cap + center content width on wide viewports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the web the app runs full-width, so lists, cards, forms, and chat bubbles stretched edge-to-edge and were hard to scan on a desktop monitor. Add a shared `webContentMaxWidth` (820px reading column, centered) and apply it to the main scroll surfaces: the object list/detail/form renderers, the AI assistant (message column + input bar), and the tab screens (home/apps/search/more/ notifications/profile). It is a strict no-op on native and on narrow web — `maxWidth` simply never binds below 820px — so the phone layout is unchanged (verified: identical at 375px). The dashboard is intentionally left full-width; its responsive grid sizes widgets from the window width and benefits from the space. Per-row cap on the list keeps FlashList's virtualization intact (its contentContainerStyle is padding-only). Verified in the browser at desktop width: list cards, detail, and the AI chat sit in a centered ~820px column; at mobile width everything fills the viewport as before. 1281 tests pass, typecheck + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/(tabs)/apps.tsx | 2 ++ app/(tabs)/index.tsx | 2 ++ app/(tabs)/more.tsx | 7 ++++++- app/(tabs)/notifications.tsx | 2 ++ app/(tabs)/profile.tsx | 2 ++ app/(tabs)/search.tsx | 2 ++ app/ai.tsx | 9 +++++++-- components/renderers/DetailViewRenderer.tsx | 2 ++ components/renderers/FormViewRenderer.tsx | 2 ++ components/renderers/ListViewRenderer.tsx | 6 +++++- lib/responsive.ts | 18 ++++++++++++++++++ 11 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 lib/responsive.ts diff --git a/app/(tabs)/apps.tsx b/app/(tabs)/apps.tsx index d353f43..a294ca3 100644 --- a/app/(tabs)/apps.tsx +++ b/app/(tabs)/apps.tsx @@ -1,5 +1,6 @@ import { View, Text, ScrollView, RefreshControl } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { webContentMaxWidth } from "~/lib/responsive"; import { useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; import { useCallback, useState } from "react"; @@ -34,6 +35,7 @@ export default function AppsScreen() { diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 5a3de4c..e3f39a8 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,5 +1,6 @@ import { View, Text, ScrollView, RefreshControl } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { webContentMaxWidth } from "~/lib/responsive"; import { LayoutDashboard, ChevronRight, @@ -115,6 +116,7 @@ export default function HomeScreen() { diff --git a/app/(tabs)/more.tsx b/app/(tabs)/more.tsx index 11ff2d4..864d381 100644 --- a/app/(tabs)/more.tsx +++ b/app/(tabs)/more.tsx @@ -1,5 +1,6 @@ import { View, Text, ScrollView, TouchableOpacity } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { webContentMaxWidth } from "~/lib/responsive"; import { UserCircle, Bell, @@ -81,7 +82,11 @@ export default function MoreScreen() { return ( - + {/* Profile Header */} {notifications.map((n) => ( diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index ba420b2..226e554 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -1,5 +1,6 @@ import { View, Text, ScrollView } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { webContentMaxWidth } from "~/lib/responsive"; import { UserCircle } from "lucide-react-native"; import { useRouter } from "expo-router"; import { Button } from "~/components/ui/Button"; @@ -37,6 +38,7 @@ export default function ProfileScreen() { diff --git a/app/(tabs)/search.tsx b/app/(tabs)/search.tsx index 2020227..64339bc 100644 --- a/app/(tabs)/search.tsx +++ b/app/(tabs)/search.tsx @@ -1,5 +1,6 @@ import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { webContentMaxWidth } from "~/lib/responsive"; import { Search as SearchIcon, X, ChevronRight, FileText, SearchX } from "lucide-react-native"; import { useRouter } from "expo-router"; import { useTranslation } from "react-i18next"; @@ -68,6 +69,7 @@ export default function SearchScreen() { diff --git a/app/ai.tsx b/app/ai.tsx index 9328f6b..f682bd9 100644 --- a/app/ai.tsx +++ b/app/ai.tsx @@ -11,6 +11,7 @@ import { } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { useTranslation } from "react-i18next"; +import { webContentMaxWidth } from "~/lib/responsive"; import * as Clipboard from "expo-clipboard"; import { Send, @@ -305,6 +306,7 @@ export default function AIAssistantScreen() { ref={scrollRef} className="flex-1" contentContainerClassName="px-4 py-4" + contentContainerStyle={webContentMaxWidth} keyboardShouldPersistTaps="handled" > {messages.length === 0 ? ( @@ -356,8 +358,11 @@ export default function AIAssistantScreen() { )} - {/* Input bar */} - + {/* Input bar — capped + centered on web to align with the message column */} + {/* Field sections */} {sections.map((section, idx) => { diff --git a/components/renderers/FormViewRenderer.tsx b/components/renderers/FormViewRenderer.tsx index 462a6f8..f55e77d 100644 --- a/components/renderers/FormViewRenderer.tsx +++ b/components/renderers/FormViewRenderer.tsx @@ -9,6 +9,7 @@ import { } from "react-native"; import { ChevronDown, ChevronUp } from "lucide-react-native"; import { Button } from "~/components/ui/Button"; +import { webContentMaxWidth } from "~/lib/responsive"; import { isFieldVisible, isFieldReadonlyByCondition, @@ -310,6 +311,7 @@ export function FormViewRenderer({ {sections.map((section, idx) => { diff --git a/components/renderers/ListViewRenderer.tsx b/components/renderers/ListViewRenderer.tsx index ce43f16..4e8c402 100644 --- a/components/renderers/ListViewRenderer.tsx +++ b/components/renderers/ListViewRenderer.tsx @@ -7,6 +7,7 @@ import { RefreshControl, } from "react-native"; import { FlashList } from "@shopify/flash-list"; +import { webContentMaxWidth } from "~/lib/responsive"; import { ChevronDown, ChevronUp, Check, Search as SearchIcon, AlertCircle } from "lucide-react-native"; import { cn } from "~/lib/utils"; import { EmptyState } from "~/components/common/EmptyState"; @@ -392,7 +393,10 @@ export function ListViewRenderer({ const rowContent = (