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:
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
};
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user