mobile: consistent dark theme as desktop

This commit is contained in:
talksik
2026-04-30 17:46:09 -07:00
parent 424f27737b
commit 13d3df2b51
8 changed files with 74 additions and 45 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ const config: ExpoConfig = {
orientation: "portrait", orientation: "portrait",
icon: "./assets/icon.png", icon: "./assets/icon.png",
scheme: "flowy", scheme: "flowy",
userInterfaceStyle: "automatic", userInterfaceStyle: "dark",
newArchEnabled: true, newArchEnabled: true,
splash: { splash: {
image: "./assets/icon.png", image: "./assets/icon.png",
+1 -1
View File
@@ -31,7 +31,7 @@ export default function App() {
<RootNavigator /> <RootNavigator />
</NavigationContainer> </NavigationContainer>
<Toaster /> <Toaster />
<StatusBar style="auto" /> <StatusBar style="light" />
</SafeAreaProvider> </SafeAreaProvider>
</PusherProvider> </PusherProvider>
</QueryClientProvider> </QueryClientProvider>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
import { View } from "react-native";
/**
* Inset hairline separator for edge-to-edge list rows. Pass to a FlatList
* via `ItemSeparatorComponent` so it renders only between rows, not after
* the last one. Mirrors desktop's <Separator className="mx-4" /> pattern.
*/
export function ListSeparator() {
return <View className="bg-border mx-4 h-px" />;
}
@@ -13,6 +13,8 @@ import { useNetworks } from "@/hooks/use-networks";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import { toUserMessage } from "@/lib/errors"; import { toUserMessage } from "@/lib/errors";
import type { RootStackScreenProps } from "@/navigation/types"; import type { RootStackScreenProps } from "@/navigation/types";
import { FlowyLogo } from "@/components/FlowyLogo";
import { ListSeparator } from "@/components/ListSeparator";
import { Drawer } from "./Drawer"; import { Drawer } from "./Drawer";
export function NetworkListScreen({ export function NetworkListScreen({
@@ -48,7 +50,7 @@ export function NetworkListScreen({
{initials} {initials}
</Text> </Text>
</Pressable> </Pressable>
<Text className="text-foreground text-base font-semibold">Flowy</Text> <FlowyLogo />
<View className="w-9" /> <View className="w-9" />
</View> </View>
@@ -71,10 +73,10 @@ export function NetworkListScreen({
<FlatList <FlatList
data={data} data={data}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
contentContainerClassName="p-4 gap-2"
refreshControl={ refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
} }
ItemSeparatorComponent={ListSeparator}
renderItem={({ item }) => ( renderItem={({ item }) => (
<NetworkCard <NetworkCard
network={item} network={item}
@@ -106,10 +108,10 @@ function NetworkCard({
return ( return (
<Pressable <Pressable
onPress={onPress} onPress={onPress}
className="bg-card border-border active:bg-accent rounded-lg border px-4 py-4 flex-row items-center justify-between" className="active:bg-accent flex-row items-center justify-between px-4 py-4"
> >
<View className="flex-1"> <View className="flex-1">
<Text className="text-card-foreground text-base font-semibold"> <Text className="text-foreground text-base font-semibold">
{network.name} {network.name}
</Text> </Text>
<Text className="text-muted-foreground text-sm"> <Text className="text-muted-foreground text-sm">
@@ -96,10 +96,7 @@ export const StreamCard = memo(function StreamCard({
<Pressable <Pressable
onPress={onPress} onPress={onPress}
android_ripple={{ color: "rgba(0,0,0,0.05)" }} android_ripple={{ color: "rgba(0,0,0,0.05)" }}
className={cn( className="bg-card flex-row items-center gap-3 px-4 py-3 active:bg-accent"
"bg-card border-b px-3.5 py-3 flex-row items-center gap-3 active:bg-accent",
isUnseen ? "border-primary" : "border-border",
)}
> >
<View <View
className={cn( className={cn(
@@ -6,6 +6,7 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context"; import { SafeAreaView } from "react-native-safe-area-context";
import { ListSeparator } from "@/components/ListSeparator";
import { toUserMessage } from "@/lib/errors"; import { toUserMessage } from "@/lib/errors";
import { particlePath } from "@/lib/particle-path"; import { particlePath } from "@/lib/particle-path";
import { useNetwork } from "@/hooks/use-networks"; import { useNetwork } from "@/hooks/use-networks";
@@ -43,7 +44,7 @@ export function StreamListScreen({
<FlatList <FlatList
data={streams} data={streams}
keyExtractor={(s) => s.id} keyExtractor={(s) => s.id}
contentContainerClassName="" ItemSeparatorComponent={ListSeparator}
renderItem={({ item }) => ( renderItem={({ item }) => (
<StreamCard <StreamCard
particle={item} particle={item}
+33 -34
View File
@@ -1,45 +1,44 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
// Tokens mirror js/desktop/src/styles/globals.css. Desktop authors values in // Tokens mirror js/desktop/src/styles/globals.css `.dark` palette. Desktop
// OKLCH; React Native's color parser is not guaranteed to handle oklch(), so // authors values in OKLCH; React Native's color parser is not guaranteed to
// we ship hex equivalents here and keep the OKLCH source in comments. // handle oklch(), so we ship hex/rgba equivalents here and keep the OKLCH
// source in comments.
// //
// Names match desktop 1:1 components written against `bg-background`, // Mobile is dark-only by design — names match desktop 1:1 so components
// `text-foreground`, etc. behave identically. // written against `bg-background`, `text-foreground`, etc. behave identically
// to the desktop app in dark mode.
module.exports = { module.exports = {
content: ["./index.ts", "./src/**/*.{ts,tsx}"], content: ["./index.ts", "./src/**/*.{ts,tsx}"],
presets: [require("nativewind/preset")], presets: [require("nativewind/preset")],
darkMode: "class",
theme: { theme: {
extend: { extend: {
colors: { colors: {
// ---- Light tokens (desktop :root) ---- background: "#252525", // oklch(0.145 0 0)
background: { DEFAULT: "#ffffff", dark: "#252525" }, // oklch(1 0 0) / oklch(0.145 0 0) foreground: "#fafafa", // oklch(0.985 0 0)
foreground: { DEFAULT: "#252525", dark: "#fafafa" }, // oklch(0.145 0 0) / oklch(0.985 0 0) card: "#363636", // oklch(0.205 0 0)
card: { DEFAULT: "#ffffff", dark: "#363636" }, // oklch(1 0 0) / oklch(0.205 0 0) "card-foreground": "#fafafa",
"card-foreground": { DEFAULT: "#252525", dark: "#fafafa" }, popover: "#363636",
popover: { DEFAULT: "#ffffff", dark: "#363636" }, "popover-foreground": "#fafafa",
"popover-foreground": { DEFAULT: "#252525", dark: "#fafafa" }, primary: "#ebebeb", // oklch(0.922 0 0)
primary: { DEFAULT: "#363636", dark: "#ebebeb" }, // oklch(0.205 0 0) / oklch(0.922 0 0) "primary-foreground": "#363636",
"primary-foreground": { DEFAULT: "#fafafa", dark: "#363636" }, secondary: "#454545", // oklch(0.269 0 0)
secondary: { DEFAULT: "#f4f4f4", dark: "#454545" }, // oklch(0.97 0 0) / oklch(0.269 0 0) "secondary-foreground": "#fafafa",
"secondary-foreground": { DEFAULT: "#363636", dark: "#fafafa" }, muted: "#454545",
muted: { DEFAULT: "#f4f4f4", dark: "#454545" }, "muted-foreground": "#a6a6a6", // oklch(0.708 0 0)
"muted-foreground": { DEFAULT: "#878787", dark: "#a6a6a6" }, // oklch(0.556 0 0) / oklch(0.708 0 0) accent: "#454545",
accent: { DEFAULT: "#f4f4f4", dark: "#454545" }, "accent-foreground": "#fafafa",
"accent-foreground": { DEFAULT: "#363636", dark: "#fafafa" }, destructive: "#ef4444", // oklch(0.704 0.191 22.216)
destructive: { DEFAULT: "#dc2626", dark: "#ef4444" }, // oklch(0.577 0.245 27.325) / oklch(0.704 0.191 22.216) border: "rgba(255,255,255,0.1)", // oklch(1 0 0 / 10%)
border: { DEFAULT: "#dcdcdc", dark: "rgba(255,255,255,0.1)" }, input: "rgba(255,255,255,0.15)", // oklch(1 0 0 / 15%)
input: { DEFAULT: "#dcdcdc", dark: "rgba(255,255,255,0.15)" }, ring: "#878787", // oklch(0.556 0 0)
ring: { DEFAULT: "#a6a6a6", dark: "#878787" }, // oklch(0.708 0 0) / oklch(0.556 0 0) sidebar: "#363636",
// Sidebar (drawer) tokens — desktop also defines these "sidebar-foreground": "#fafafa",
sidebar: { DEFAULT: "#fafafa", dark: "#363636" }, "sidebar-primary": "#6366f1", // chart-1 dark = oklch(0.488 0.243 264.376)
"sidebar-foreground": { DEFAULT: "#252525", dark: "#fafafa" }, "sidebar-primary-foreground": "#fafafa",
"sidebar-primary": { DEFAULT: "#363636", dark: "#6366f1" }, // chart-1 dark = oklch(0.488 0.243 264.376) "sidebar-accent": "#454545",
"sidebar-primary-foreground": { DEFAULT: "#fafafa", dark: "#fafafa" }, "sidebar-accent-foreground": "#fafafa",
"sidebar-accent": { DEFAULT: "#f4f4f4", dark: "#454545" }, "sidebar-border": "rgba(255,255,255,0.1)",
"sidebar-accent-foreground": { DEFAULT: "#363636", dark: "#fafafa" }, "sidebar-ring": "#878787",
"sidebar-border": { DEFAULT: "#dcdcdc", dark: "rgba(255,255,255,0.1)" },
"sidebar-ring": { DEFAULT: "#a6a6a6", dark: "#878787" },
}, },
borderRadius: { borderRadius: {
lg: "10px", // --radius: 0.625rem lg: "10px", // --radius: 0.625rem