feat: add theme selector with multiple dark themes

Add theme dropdown in settings with Light, Dark, System plus
Zinc, Midnight, Claude, and Ghostty dark variants. Each theme
has its own accent color and surface tints. Desaturate terminal
ANSI colors across all dark themes. Add surfaceSidebarHover
semantic token so hover states go the right direction in both
light and dark modes. Self-heal if a stored theme no longer exists.
This commit is contained in:
Mohamed Boudra
2026-04-10 16:21:05 +07:00
parent 120c1b46a4
commit 2f77674c55
9 changed files with 375 additions and 135 deletions
+2 -1
View File
@@ -14,6 +14,7 @@ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { PortalProvider } from "@gorhom/portal"; import { PortalProvider } from "@gorhom/portal";
import { VoiceProvider } from "@/contexts/voice-context"; import { VoiceProvider } from "@/contexts/voice-context";
import { useAppSettings } from "@/hooks/use-settings"; import { useAppSettings } from "@/hooks/use-settings";
import { THEME_TO_UNISTYLES } from "@/styles/theme";
import { useFaviconStatus } from "@/hooks/use-favicon-status"; import { useFaviconStatus } from "@/hooks/use-favicon-status";
import { View, Text } from "react-native"; import { View, Text } from "react-native";
import { UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { UnistylesRuntime, useUnistyles } from "react-native-unistyles";
@@ -561,7 +562,7 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
UnistylesRuntime.setAdaptiveThemes(true); UnistylesRuntime.setAdaptiveThemes(true);
} else { } else {
UnistylesRuntime.setAdaptiveThemes(false); UnistylesRuntime.setAdaptiveThemes(false);
UnistylesRuntime.setTheme(settings.theme); UnistylesRuntime.setTheme(THEME_TO_UNISTYLES[settings.theme]);
} }
}, [settingsLoading, settings.theme]); }, [settingsLoading, settings.theme]);
@@ -472,7 +472,7 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
}, },
tabActive: { tabActive: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
tabText: { tabText: {
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
@@ -849,7 +849,7 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
}, },
entryRowActive: { entryRowActive: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
indentGuide: { indentGuide: {
position: "absolute", position: "absolute",
+3 -3
View File
@@ -885,10 +885,10 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground, color: theme.colors.foreground,
}, },
newAgentButtonHovered: { newAgentButtonHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
newAgentButtonActive: { newAgentButtonActive: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
hostTrigger: { hostTrigger: {
flexDirection: "row", flexDirection: "row",
@@ -901,7 +901,7 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
}, },
hostTriggerHovered: { hostTriggerHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
hostStatusDot: { hostStatusDot: {
width: 8, width: 8,
@@ -1980,7 +1980,7 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
projectRowHovered: { projectRowHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
projectRowPressed: { projectRowPressed: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
@@ -2050,7 +2050,7 @@ const styles = StyleSheet.create((theme) => ({
flexShrink: 0, flexShrink: 0,
}, },
projectActionButtonHovered: { projectActionButtonHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
projectActionButtonText: { projectActionButtonText: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
@@ -2065,7 +2065,7 @@ const styles = StyleSheet.create((theme) => ({
flexShrink: 0, flexShrink: 0,
}, },
projectIconActionButtonHovered: { projectIconActionButtonHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
projectIconActionButtonHidden: { projectIconActionButtonHidden: {
opacity: 0, opacity: 0,
@@ -2143,7 +2143,7 @@ const styles = StyleSheet.create((theme) => ({
flexShrink: 0, flexShrink: 0,
}, },
workspaceRowHovered: { workspaceRowHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
workspaceRowPressed: { workspaceRowPressed: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
@@ -2157,7 +2157,7 @@ const styles = StyleSheet.create((theme) => ({
...theme.shadow.md, ...theme.shadow.md,
}, },
sidebarRowSelected: { sidebarRowSelected: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
workspaceRowContainer: { workspaceRowContainer: {
position: "relative", position: "relative",
+8 -1
View File
@@ -6,10 +6,14 @@ export const APP_SETTINGS_KEY = "@paseo:app-settings";
const LEGACY_SETTINGS_KEY = "@paseo:settings"; const LEGACY_SETTINGS_KEY = "@paseo:settings";
const APP_SETTINGS_QUERY_KEY = ["app-settings"]; const APP_SETTINGS_QUERY_KEY = ["app-settings"];
import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme";
export type SendBehavior = "interrupt" | "queue"; export type SendBehavior = "interrupt" | "queue";
const VALID_THEMES = new Set<string>([...Object.keys(THEME_TO_UNISTYLES), "auto"]);
export interface AppSettings { export interface AppSettings {
theme: "dark" | "light" | "auto"; theme: ThemeName | "auto";
manageBuiltInDaemon: boolean; manageBuiltInDaemon: boolean;
sendBehavior: SendBehavior; sendBehavior: SendBehavior;
} }
@@ -78,6 +82,9 @@ export async function loadSettingsFromStorage(): Promise<AppSettings> {
const stored = await AsyncStorage.getItem(APP_SETTINGS_KEY); const stored = await AsyncStorage.getItem(APP_SETTINGS_KEY);
if (stored) { if (stored) {
const parsed = JSON.parse(stored) as Partial<AppSettings>; const parsed = JSON.parse(stored) as Partial<AppSettings>;
if (parsed.theme && !VALID_THEMES.has(parsed.theme)) {
parsed.theme = DEFAULT_APP_SETTINGS.theme;
}
return { ...DEFAULT_APP_SETTINGS, ...parsed }; return { ...DEFAULT_APP_SETTINGS, ...parsed };
} }
+96 -23
View File
@@ -10,6 +10,7 @@ import {
Sun, Sun,
Moon, Moon,
Monitor, Monitor,
ChevronDown,
Globe, Globe,
Settings, Settings,
RotateCw, RotateCw,
@@ -24,6 +25,7 @@ import {
Smartphone, Smartphone,
} from "lucide-react-native"; } from "lucide-react-native";
import { useAppSettings, type AppSettings, type SendBehavior } from "@/hooks/use-settings"; import { useAppSettings, type AppSettings, type SendBehavior } from "@/hooks/use-settings";
import { THEME_SWATCHES, type ThemeName } from "@/styles/theme";
import type { HostProfile, HostConnection } from "@/types/host-connection"; import type { HostProfile, HostConnection } from "@/types/host-connection";
import { useHosts, useHostMutations } from "@/runtime/host-runtime"; import { useHosts, useHostMutations } from "@/runtime/host-runtime";
import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons"; import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons";
@@ -47,6 +49,7 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet"; import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet";
@@ -362,11 +365,53 @@ interface GeneralSectionProps {
handleSendBehaviorChange: (behavior: SendBehavior) => void; handleSendBehaviorChange: (behavior: SendBehavior) => void;
} }
function ThemeIcon({ theme, size, color }: { theme: AppSettings["theme"]; size: number; color: string }) {
switch (theme) {
case "light":
return <Sun size={size} color={color} />;
case "dark":
return <Moon size={size} color={color} />;
case "auto":
return <Monitor size={size} color={color} />;
default:
return <ThemeSwatch color={THEME_SWATCHES[theme]} size={size} />;
}
}
function ThemeSwatch({ color, size }: { color: string; size: number }) {
return (
<View
style={{
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: color,
borderWidth: 1,
borderColor: "rgba(255,255,255,0.15)",
}}
/>
);
}
const THEME_LABELS: Record<AppSettings["theme"], string> = {
light: "Light",
dark: "Dark",
zinc: "Zinc",
midnight: "Midnight",
claude: "Claude",
ghostty: "Ghostty",
auto: "System",
};
function GeneralSection({ function GeneralSection({
settings, settings,
handleThemeChange, handleThemeChange,
handleSendBehaviorChange, handleSendBehaviorChange,
}: GeneralSectionProps) { }: GeneralSectionProps) {
const { theme } = useUnistyles();
const iconSize = theme.iconSize.md;
const iconColor = theme.colors.foregroundMuted;
return ( return (
<View style={settingsStyles.section}> <View style={settingsStyles.section}>
<Text style={settingsStyles.sectionTitle}>General</Text> <Text style={settingsStyles.sectionTitle}>General</Text>
@@ -375,29 +420,43 @@ function GeneralSection({
<View style={styles.audioRowContent}> <View style={styles.audioRowContent}>
<Text style={styles.audioRowTitle}>Theme</Text> <Text style={styles.audioRowTitle}>Theme</Text>
</View> </View>
<SegmentedControl <DropdownMenu>
size="sm" <DropdownMenuTrigger
hideLabels={Platform.OS !== "web"} style={({ pressed }) => [
value={settings.theme} styles.themeTrigger,
onValueChange={handleThemeChange} pressed && { opacity: 0.85 },
options={[ ]}
{ >
value: "light", <ThemeIcon theme={settings.theme} size={iconSize} color={iconColor} />
label: "Light", <Text style={styles.themeTriggerText}>
icon: ({ color, size }) => <Sun size={size} color={color} />, {THEME_LABELS[settings.theme]}
}, </Text>
{ <ChevronDown size={theme.iconSize.sm} color={iconColor} />
value: "dark", </DropdownMenuTrigger>
label: "Dark", <DropdownMenuContent side="bottom" align="end" width={200}>
icon: ({ color, size }) => <Moon size={size} color={color} />, {(["light", "dark", "auto"] as const).map((t) => (
}, <DropdownMenuItem
{ key={t}
value: "auto", selected={settings.theme === t}
label: "System", onSelect={() => handleThemeChange(t)}
icon: ({ color, size }) => <Monitor size={size} color={color} />, leading={<ThemeIcon theme={t} size={iconSize} color={iconColor} />}
}, >
]} {THEME_LABELS[t]}
/> </DropdownMenuItem>
))}
<DropdownMenuSeparator />
{(["zinc", "midnight", "claude", "ghostty"] as const).map((t) => (
<DropdownMenuItem
key={t}
selected={settings.theme === t}
onSelect={() => handleThemeChange(t)}
leading={<ThemeIcon theme={t} size={iconSize} color={iconColor} />}
>
{THEME_LABELS[t]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</View> </View>
<View style={styles.audioRow}> <View style={styles.audioRow}>
<View style={styles.audioRowContent}> <View style={styles.audioRowContent}>
@@ -1806,6 +1865,20 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium, fontWeight: theme.fontWeight.medium,
}, },
themeTrigger: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.md,
borderWidth: 1,
borderColor: theme.colors.border,
},
themeTriggerText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
disabled: { disabled: {
opacity: theme.opacity[50], opacity: theme.opacity[50],
}, },
+243 -96
View File
@@ -103,6 +103,8 @@ export const baseColors = {
}, },
} as const; } as const;
export type ThemeName = "light" | "dark" | "zinc" | "midnight" | "claude" | "ghostty";
// Semantic color tokens - Layer-based system // Semantic color tokens - Layer-based system
const lightSemanticColors = { const lightSemanticColors = {
// Surfaces (layers) - shifted one step lighter // Surfaces (layers) - shifted one step lighter
@@ -113,10 +115,11 @@ const lightSemanticColors = {
surface4: "#d4d4d8", // Extra emphasis (was zinc-400, now zinc-300) surface4: "#d4d4d8", // Extra emphasis (was zinc-400, now zinc-300)
surfaceDiffEmpty: "#f6f6f6", // Empty side of split diff rows, between surface1 and surface2 and biased toward surface2 surfaceDiffEmpty: "#f6f6f6", // Empty side of split diff rows, between surface1 and surface2 and biased toward surface2
surfaceSidebar: "#f4f4f5", // Sidebar background (darker than main) surfaceSidebar: "#f4f4f5", // Sidebar background (darker than main)
surfaceSidebarHover: "#e9e9ec", // Sidebar hover (darker in light mode)
surfaceWorkspace: "#ffffff", // Workspace main background surfaceWorkspace: "#ffffff", // Workspace main background
// Text // Text
foreground: "#09090b", foreground: "#1a1a1e",
foregroundMuted: "#71717a", foregroundMuted: "#71717a",
// Controls // Controls
@@ -140,11 +143,11 @@ const lightSemanticColors = {
// Legacy aliases (for gradual migration) // Legacy aliases (for gradual migration)
background: "#ffffff", background: "#ffffff",
popover: "#ffffff", popover: "#ffffff",
popoverForeground: "#09090b", popoverForeground: "#1a1a1e",
primary: "#18181b", primary: "#18181b",
primaryForeground: "#fafafa", primaryForeground: "#fafafa",
secondary: "#f4f4f5", secondary: "#f4f4f5",
secondaryForeground: "#09090b", secondaryForeground: "#1a1a1e",
muted: "#f4f4f5", muted: "#f4f4f5",
mutedForeground: "#71717a", mutedForeground: "#71717a",
accentBorder: "#ececf1", accentBorder: "#ececf1",
@@ -153,13 +156,13 @@ const lightSemanticColors = {
terminal: { terminal: {
background: "#ffffff", background: "#ffffff",
foreground: "#09090b", foreground: "#1a1a1e",
cursor: "#09090b", cursor: "#1a1a1e",
cursorAccent: "#ffffff", cursorAccent: "#ffffff",
selectionBackground: "rgba(0, 0, 0, 0.15)", selectionBackground: "rgba(0, 0, 0, 0.15)",
selectionForeground: "#09090b", selectionForeground: "#1a1a1e",
black: "#09090b", black: "#1a1a1e",
red: "#dc2626", red: "#dc2626",
green: "#16a34a", green: "#16a34a",
yellow: "#ca8a04", yellow: "#ca8a04",
@@ -179,80 +182,194 @@ const lightSemanticColors = {
}, },
} as const; } as const;
const darkSemanticColors = { // ---------------------------------------------------------------------------
// Surfaces (layers) — subtle teal tint // Dark theme variant builder
surface0: "#181B1A", // App background // ---------------------------------------------------------------------------
surface1: "#1E2120", // Subtle hover
surface2: "#272A29", // Elevated: badges, inputs, sheets
surface3: "#434645", // Highest elevation
surface4: "#595B5B", // Extra emphasis
surfaceDiffEmpty: "#252827", // Empty side of split diff rows, between surface1 and surface2 and biased toward surface2
surfaceSidebar: "#141716", // Sidebar background (darker than main)
surfaceWorkspace: "#1E2120", // Workspace main background (surface1)
// Text interface DarkThemeConfig {
foreground: "#fafafa", surface0: string;
surface1: string;
surface2: string;
surface3: string;
surface4: string;
surfaceDiffEmpty: string;
surfaceSidebar: string;
surfaceSidebarHover: string;
foregroundMuted: string;
scrollbarHandle: string;
border: string;
borderAccent: string;
accent: string;
accentBright: string;
}
const darkTerminalAnsi = {
red: "#e07070",
green: "#5dba80",
yellow: "#d4a44a",
blue: "#6a9de0",
magenta: "#b07ad0",
cyan: "#4aabb8",
white: "#d4d4d8",
brightRed: "#e89090",
brightGreen: "#7ecf9a",
brightYellow: "#e0be6e",
brightBlue: "#8ab4e8",
brightMagenta: "#c49ae0",
brightCyan: "#6ec2cc",
brightWhite: "#f0f0f2",
} as const;
function buildDarkSemanticColors(tint: DarkThemeConfig) {
return {
surface0: tint.surface0,
surface1: tint.surface1,
surface2: tint.surface2,
surface3: tint.surface3,
surface4: tint.surface4,
surfaceDiffEmpty: tint.surfaceDiffEmpty,
surfaceSidebar: tint.surfaceSidebar,
surfaceSidebarHover: tint.surfaceSidebarHover,
surfaceWorkspace: tint.surface1,
foreground: "#fafafa",
foregroundMuted: tint.foregroundMuted,
scrollbarHandle: tint.scrollbarHandle,
border: tint.border,
borderAccent: tint.borderAccent,
accent: tint.accent,
accentBright: tint.accentBright,
accentForeground: "#ffffff",
destructive: "#ef4444",
destructiveForeground: "#ffffff",
success: tint.accent,
successForeground: "#ffffff",
// Legacy aliases (for gradual migration)
background: tint.surface0,
popover: tint.surface2,
popoverForeground: "#fafafa",
primary: "#fafafa",
primaryForeground: tint.surface0,
secondary: tint.surface2,
secondaryForeground: "#fafafa",
muted: tint.surface2,
mutedForeground: tint.foregroundMuted,
accentBorder: tint.borderAccent,
input: tint.surface2,
ring: "#d4d4d8",
terminal: {
background: tint.surface0,
foreground: "#fafafa",
cursor: "#fafafa",
cursorAccent: tint.surface0,
selectionBackground: "rgba(255, 255, 255, 0.2)",
selectionForeground: "#fafafa",
black: tint.surfaceSidebar,
...darkTerminalAnsi,
brightBlack: tint.surface3,
},
};
}
// ---------------------------------------------------------------------------
// Dark tint definitions
// ---------------------------------------------------------------------------
// Paseo — subtle teal-green tint (default)
const paseoDarkColors = buildDarkSemanticColors({
surface0: "#181B1A",
surface1: "#1E2120",
surface2: "#272A29",
surface3: "#434645",
surface4: "#595B5B",
surfaceDiffEmpty: "#252827",
surfaceSidebar: "#141716",
surfaceSidebarHover: "#1c1f1e",
foregroundMuted: "#A1A5A4", foregroundMuted: "#A1A5A4",
scrollbarHandle: "#717574",
// Controls
scrollbarHandle: "#717574", // zinc-500 w/ teal tint
// Borders
border: "#252B2A", border: "#252B2A",
borderAccent: "#2F3534", borderAccent: "#2F3534",
// Brand
accent: "#20744A", accent: "#20744A",
accentBright: "#7ccba0", accentBright: "#7ccba0",
accentForeground: "#ffffff", });
// Semantic // Zinc — neutral gray, no tint
destructive: "#ef4444", const zincDarkColors = buildDarkSemanticColors({
destructiveForeground: "#ffffff", surface0: "#18181b",
success: "#20744A", surface1: "#1f1f22",
successForeground: "#ffffff", surface2: "#27272a",
surface3: "#3f3f46",
surface4: "#52525b",
surfaceDiffEmpty: "#242427",
surfaceSidebar: "#131316",
surfaceSidebarHover: "#1b1b1e",
foregroundMuted: "#a1a1aa",
scrollbarHandle: "#71717a",
border: "#27272a",
borderAccent: "#303036",
accent: "#20744A",
accentBright: "#7ccba0",
});
// Legacy aliases (for gradual migration) // Midnight — subtle blue tint
background: "#181B1A", const midnightDarkColors = buildDarkSemanticColors({
popover: "#272A29", surface0: "#161820",
popoverForeground: "#fafafa", surface1: "#1c1e27",
primary: "#fafafa", surface2: "#252731",
primaryForeground: "#181B1A", surface3: "#3c3e4c",
secondary: "#272A29", surface4: "#535564",
secondaryForeground: "#fafafa", surfaceDiffEmpty: "#222430",
muted: "#272A29", surfaceSidebar: "#121420",
mutedForeground: "#A1A5A4", surfaceSidebarHover: "#1a1c28",
accentBorder: "#2F3534", foregroundMuted: "#9a9db0",
input: "#272A29", scrollbarHandle: "#6b6e82",
ring: "#d4d4d8", border: "#242636",
borderAccent: "#2e3040",
accent: "#3b6fcf",
accentBright: "#7eaaeb",
});
terminal: { // Claude — warm neutral with subtle orange undertone
background: "#181B1A", const claudeDarkColors = buildDarkSemanticColors({
foreground: "#fafafa", surface0: "#1f1f1e",
cursor: "#fafafa", surface1: "#262523",
cursorAccent: "#181B1A", surface2: "#2f2d2b",
selectionBackground: "rgba(255, 255, 255, 0.2)", surface3: "#4a4745",
selectionForeground: "#fafafa", surface4: "#605d5b",
surfaceDiffEmpty: "#2a2826",
surfaceSidebar: "#1a1918",
surfaceSidebarHover: "#222120",
foregroundMuted: "#ada9a5",
scrollbarHandle: "#78746f",
border: "#2c2a27",
borderAccent: "#36332f",
accent: "#d97757",
accentBright: "#e89a7f",
});
black: "#141716", // Ghostty — blue-tinted dark based on Ghostty default background
red: "#ef4444", const ghosttyDarkColors = buildDarkSemanticColors({
green: "#22c55e", surface0: "#282c34",
yellow: "#f59e0b", surface1: "#2f333d",
blue: "#3b82f6", surface2: "#383c48",
magenta: "#a855f7", surface3: "#4a4f5e",
cyan: "#06b6d4", surface4: "#5b6175",
white: "#e4e4e7", surfaceDiffEmpty: "#323643",
surfaceSidebar: "#21252d",
brightBlack: "#434645", surfaceSidebarHover: "#292d36",
brightRed: "#f87171", foregroundMuted: "#c8ccd8",
brightGreen: "#4ade80", scrollbarHandle: "#a0a4b2",
brightYellow: "#fbbf24", border: "#353a47",
brightBlue: "#60a5fa", borderAccent: "#3f4454",
brightMagenta: "#c084fc", accent: "#89b4fa",
brightCyan: "#22d3ee", accentBright: "#b4d0fc",
brightWhite: "#ffffff", });
},
} as const;
const commonTheme = { const commonTheme = {
spacing: { spacing: {
@@ -323,35 +440,45 @@ const commonTheme = {
}, },
} as const; } as const;
export const darkTheme = { const darkShadow = {
colorScheme: "dark" as const, sm: {
colors: { shadowColor: "rgba(0, 0, 0, 0.25)",
...darkSemanticColors, shadowOffset: { width: 0, height: 2 },
palette: baseColors, shadowRadius: 4,
elevation: 2,
}, },
shadow: { md: {
sm: { shadowColor: "rgba(0, 0, 0, 0.20)",
shadowColor: "rgba(0, 0, 0, 0.25)", shadowOffset: { width: 0, height: 4 },
shadowOffset: { width: 0, height: 2 }, shadowRadius: 8,
shadowRadius: 4, elevation: 8,
elevation: 2, },
}, lg: {
md: { shadowColor: "rgba(0, 0, 0, 0.40)",
shadowColor: "rgba(0, 0, 0, 0.20)", shadowOffset: { width: 0, height: 12 },
shadowOffset: { width: 0, height: 4 }, shadowRadius: 24,
shadowRadius: 8, elevation: 8,
elevation: 8,
},
lg: {
shadowColor: "rgba(0, 0, 0, 0.40)",
shadowOffset: { width: 0, height: 12 },
shadowRadius: 24,
elevation: 8,
},
}, },
...commonTheme,
} as const; } as const;
function buildDarkTheme(semanticColors: ReturnType<typeof buildDarkSemanticColors>) {
return {
colorScheme: "dark" as const,
colors: {
...semanticColors,
palette: baseColors,
},
shadow: darkShadow,
...commonTheme,
} as const;
}
export const darkTheme = buildDarkTheme(paseoDarkColors);
export const darkZincTheme = buildDarkTheme(zincDarkColors);
export const darkMidnightTheme = buildDarkTheme(midnightDarkColors);
export const darkClaudeTheme = buildDarkTheme(claudeDarkColors);
export const darkGhosttyTheme = buildDarkTheme(ghosttyDarkColors);
export const lightTheme = { export const lightTheme = {
colorScheme: "light" as const, colorScheme: "light" as const,
colors: { colors: {
@@ -386,3 +513,23 @@ export const theme = darkTheme;
// Export a union type that works for both themes // Export a union type that works for both themes
export type Theme = typeof darkTheme | typeof lightTheme; export type Theme = typeof darkTheme | typeof lightTheme;
type UnistylesThemeKey = "light" | "dark" | "darkZinc" | "darkMidnight" | "darkClaude" | "darkGhostty";
export const THEME_TO_UNISTYLES: Record<ThemeName, UnistylesThemeKey> = {
light: "light",
dark: "dark",
zinc: "darkZinc",
midnight: "darkMidnight",
claude: "darkClaude",
ghostty: "darkGhostty",
};
export const THEME_SWATCHES: Record<ThemeName, string> = {
light: "#ffffff",
dark: "#2D8B62",
zinc: "#808080",
midnight: "#4A6BA8",
claude: "#D97757",
ghostty: "#8caaee",
};
+16 -4
View File
@@ -1,11 +1,21 @@
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
// import { UnistylesRuntime } from "react-native-unistyles"; import {
import { lightTheme, darkTheme } from "./theme"; lightTheme,
darkTheme,
darkZincTheme,
darkMidnightTheme,
darkClaudeTheme,
darkGhosttyTheme,
} from "./theme";
StyleSheet.configure({ StyleSheet.configure({
themes: { themes: {
light: lightTheme, light: lightTheme,
dark: darkTheme, dark: darkTheme,
darkZinc: darkZincTheme,
darkMidnight: darkMidnightTheme,
darkClaude: darkClaudeTheme,
darkGhostty: darkGhosttyTheme,
}, },
breakpoints: { breakpoints: {
xs: 0, xs: 0,
@@ -23,6 +33,10 @@ StyleSheet.configure({
type AppThemes = { type AppThemes = {
light: typeof lightTheme; light: typeof lightTheme;
dark: typeof darkTheme; dark: typeof darkTheme;
darkZinc: typeof darkZincTheme;
darkMidnight: typeof darkMidnightTheme;
darkClaude: typeof darkClaudeTheme;
darkGhostty: typeof darkGhosttyTheme;
}; };
type AppBreakpoints = { type AppBreakpoints = {
@@ -37,5 +51,3 @@ declare module "react-native-unistyles" {
export interface UnistylesThemes extends AppThemes {} export interface UnistylesThemes extends AppThemes {}
export interface UnistylesBreakpoints extends AppBreakpoints {} export interface UnistylesBreakpoints extends AppBreakpoints {}
} }
// UnistylesRuntime.setRootViewBackgroundColor(lightTheme.colors.background);