diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1c7abcc..666ab99 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,6 +7,7 @@ "mhutchie.git-graph", "esbenp.prettier-vscode", "yoavbls.pretty-ts-errors", - "ctf0.duplicated-code-new" + "ctf0.duplicated-code-new", + "github.vscode-github-actions" ] } diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 85b861c..f9e06dd 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -6,11 +6,11 @@ "type": "module", "scripts": { "start": "expo start", - "android": "expo run:android", + "android": "expo run:android --port 8082", + "ios": "expo run:ios --port 8081", "android:release": "expo run:android --variant release", "build:signed:aab": "tsx scripts/signed-build.ts", "build:signed:apk": "tsx scripts/signed-build.ts --format apk", - "ios": "expo run:ios", "web": "expo start --web", "prebuild": "expo prebuild", "prebuild:clean": "expo prebuild --clean", diff --git a/apps/mobile/src/app/(tabs)/_layout.tsx b/apps/mobile/src/app/(tabs)/_layout.tsx index c1d29d5..76df629 100644 --- a/apps/mobile/src/app/(tabs)/_layout.tsx +++ b/apps/mobile/src/app/(tabs)/_layout.tsx @@ -1,19 +1,21 @@ import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs'; import React from 'react'; +import { useTheme } from '@/lib/theme'; export default function TabsLayout() { + const theme = useTheme(); return ( - + - + - + - + diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 2f81e93..786d40f 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -22,7 +22,7 @@ import { connectionDetailsSchema, secretsManager, } from '@/lib/secrets-manager'; -import { useTheme } from '@/theme'; +import { useTheme ,type AppTheme } from '@/lib/theme'; export default function TabsIndex() { return ; @@ -40,6 +40,7 @@ const defaultValues: ConnectionDetails = { function Host() { const theme = useTheme(); + const styles = React.useMemo(() => makeStyles(theme), [theme]); const insets = useSafeAreaInsets(); const sshConnMutation = useSshConnMutation(); const connectionForm = useAppForm({ @@ -192,6 +193,8 @@ function Host() { } function KeyIdPickerField() { + const theme = useTheme(); + const styles = React.useMemo(() => makeStyles(theme), [theme]); const field = useFieldContext(); const [open, setOpen] = React.useState(false); @@ -228,7 +231,7 @@ function KeyIdPickerField() { setOpen(true); }} > - {display} + {display} {!selected && ( @@ -270,6 +273,8 @@ function KeyIdPickerField() { function PreviousConnectionsSection(props: { onSelect: (connection: ConnectionDetails) => void; }) { + const theme = useTheme(); + const styles = React.useMemo(() => makeStyles(theme), [theme]); const listConnectionsQuery = useQuery(secretsManager.connections.query.list); return ( @@ -300,6 +305,8 @@ function ConnectionRow(props: { id: string; onSelect: (connection: ConnectionDetails) => void; }) { + const theme = useTheme(); + const styles = React.useMemo(() => makeStyles(theme), [theme]); const detailsQuery = useQuery(secretsManager.connections.query.get(props.id)); const details = detailsQuery.data?.value; @@ -324,196 +331,198 @@ function ConnectionRow(props: { ); } -const styles = StyleSheet.create({ - container: { - flex: 1, - padding: 24, - backgroundColor: '#0B1324', - justifyContent: 'center', - }, - scrollContent: { - paddingBottom: 32, - }, - header: { - marginBottom: 16, - alignItems: 'center', - }, - appName: { - fontSize: 28, - fontWeight: '800', - color: '#E5E7EB', - letterSpacing: 1, - }, - appTagline: { - marginTop: 4, - fontSize: 13, - color: '#9AA0A6', - }, - card: { - backgroundColor: '#111B34', - borderRadius: 20, - padding: 24, - marginHorizontal: 4, - shadowColor: '#000', - shadowOpacity: 0.3, - shadowRadius: 16, - shadowOffset: { width: 0, height: 4 }, - elevation: 8, - borderWidth: 1, - borderColor: '#1E293B', - }, - title: { - fontSize: 24, - fontWeight: '700', - color: '#E5E7EB', - marginBottom: 6, - letterSpacing: 0.5, - }, - subtitle: { - fontSize: 15, - color: '#9AA0A6', - marginBottom: 24, - lineHeight: 20, - }, - inputGroup: { - marginBottom: 12, - }, - label: { - marginBottom: 6, - fontSize: 14, - color: '#C6CBD3', - fontWeight: '600', - }, - input: { - borderWidth: 1, - borderColor: '#2A3655', - backgroundColor: '#0E172B', - color: '#E5E7EB', - borderRadius: 10, - paddingHorizontal: 12, - paddingVertical: 12, - fontSize: 16, - }, - errorText: { - marginTop: 6, - color: '#FCA5A5', - fontSize: 12, - }, - actions: { - marginTop: 20, - }, - mutedText: { - color: '#9AA0A6', - fontSize: 14, - }, - submitButton: { - backgroundColor: '#2563EB', - borderRadius: 12, - paddingVertical: 16, - alignItems: 'center', - shadowColor: '#2563EB', - shadowOpacity: 0.3, - shadowRadius: 8, - shadowOffset: { width: 0, height: 2 }, - elevation: 4, - }, - submitButtonText: { - color: '#FFFFFF', - fontWeight: '700', - fontSize: 16, - letterSpacing: 0.5, - }, - buttonDisabled: { - backgroundColor: '#3B82F6', - opacity: 0.6, - }, - secondaryButton: { - backgroundColor: 'transparent', - borderWidth: 1, - borderColor: '#2A3655', - borderRadius: 12, - paddingVertical: 14, - alignItems: 'center', - marginTop: 12, - }, - secondaryButtonText: { - color: '#C6CBD3', - fontWeight: '600', - fontSize: 14, - letterSpacing: 0.3, - }, - listSection: { - marginTop: 20, - }, - listTitle: { - fontSize: 16, - fontWeight: '700', - color: '#E5E7EB', - marginBottom: 8, - }, - listContainer: { - // Intentionally empty for RN compatibility - }, - row: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - backgroundColor: '#0E172B', - borderWidth: 1, - borderColor: '#2A3655', - borderRadius: 12, - paddingHorizontal: 12, - paddingVertical: 12, - marginBottom: 8, - }, - rowTextContainer: { - flex: 1, - marginRight: 12, - }, - rowTitle: { - color: '#E5E7EB', - fontSize: 15, - fontWeight: '600', - }, - rowSubtitle: { - color: '#9AA0A6', - marginTop: 2, - fontSize: 12, - }, - rowChevron: { - color: '#9AA0A6', - fontSize: 22, - paddingHorizontal: 4, - }, - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.4)', - justifyContent: 'flex-end', - }, - modalSheet: { - backgroundColor: '#0B1324', - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - padding: 16, - borderColor: '#1E293B', - borderWidth: 1, - maxHeight: '85%', - }, - modalHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, - }, - modalCloseButton: { - paddingHorizontal: 8, - paddingVertical: 6, - borderRadius: 8, - borderWidth: 1, - borderColor: '#2A3655', - }, - modalCloseText: { - color: '#C6CBD3', - fontWeight: '600', - }, -}); +function makeStyles(theme: AppTheme) { + return StyleSheet.create({ + container: { + flex: 1, + padding: 24, + backgroundColor: theme.colors.background, + justifyContent: 'center', + }, + scrollContent: { + paddingBottom: 32, + }, + header: { + marginBottom: 16, + alignItems: 'center', + }, + appName: { + fontSize: 28, + fontWeight: '800', + color: theme.colors.textPrimary, + letterSpacing: 1, + }, + appTagline: { + marginTop: 4, + fontSize: 13, + color: theme.colors.muted, + }, + card: { + backgroundColor: theme.colors.surface, + borderRadius: 20, + padding: 24, + marginHorizontal: 4, + shadowColor: theme.colors.shadow, + shadowOpacity: 0.3, + shadowRadius: 16, + shadowOffset: { width: 0, height: 4 }, + elevation: 8, + borderWidth: 1, + borderColor: theme.colors.borderStrong, + }, + title: { + fontSize: 24, + fontWeight: '700', + color: theme.colors.textPrimary, + marginBottom: 6, + letterSpacing: 0.5, + }, + subtitle: { + fontSize: 15, + color: theme.colors.muted, + marginBottom: 24, + lineHeight: 20, + }, + inputGroup: { + marginBottom: 12, + }, + label: { + marginBottom: 6, + fontSize: 14, + color: theme.colors.textSecondary, + fontWeight: '600', + }, + input: { + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.inputBackground, + color: theme.colors.textPrimary, + borderRadius: 10, + paddingHorizontal: 12, + paddingVertical: 12, + fontSize: 16, + }, + errorText: { + marginTop: 6, + color: theme.colors.danger, + fontSize: 12, + }, + actions: { + marginTop: 20, + }, + mutedText: { + color: theme.colors.muted, + fontSize: 14, + }, + submitButton: { + backgroundColor: theme.colors.primary, + borderRadius: 12, + paddingVertical: 16, + alignItems: 'center', + shadowColor: theme.colors.primary, + shadowOpacity: 0.3, + shadowRadius: 8, + shadowOffset: { width: 0, height: 2 }, + elevation: 4, + }, + submitButtonText: { + color: theme.colors.buttonTextOnPrimary, + fontWeight: '700', + fontSize: 16, + letterSpacing: 0.5, + }, + buttonDisabled: { + backgroundColor: theme.colors.primaryDisabled, + opacity: 0.6, + }, + secondaryButton: { + backgroundColor: theme.colors.transparent, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: 12, + paddingVertical: 14, + alignItems: 'center', + marginTop: 12, + }, + secondaryButtonText: { + color: theme.colors.textSecondary, + fontWeight: '600', + fontSize: 14, + letterSpacing: 0.3, + }, + listSection: { + marginTop: 20, + }, + listTitle: { + fontSize: 16, + fontWeight: '700', + color: theme.colors.textPrimary, + marginBottom: 8, + }, + listContainer: { + // Intentionally empty for RN compatibility + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: theme.colors.inputBackground, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 12, + marginBottom: 8, + }, + rowTextContainer: { + flex: 1, + marginRight: 12, + }, + rowTitle: { + color: theme.colors.textPrimary, + fontSize: 15, + fontWeight: '600', + }, + rowSubtitle: { + color: theme.colors.muted, + marginTop: 2, + fontSize: 12, + }, + rowChevron: { + color: theme.colors.muted, + fontSize: 22, + paddingHorizontal: 4, + }, + modalOverlay: { + flex: 1, + backgroundColor: theme.colors.overlay, + justifyContent: 'flex-end', + }, + modalSheet: { + backgroundColor: theme.colors.background, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + padding: 16, + borderColor: theme.colors.borderStrong, + borderWidth: 1, + maxHeight: '85%', + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + modalCloseButton: { + paddingHorizontal: 8, + paddingVertical: 6, + borderRadius: 8, + borderWidth: 1, + borderColor: theme.colors.border, + }, + modalCloseText: { + color: theme.colors.textSecondary, + fontWeight: '600', + }, + }); +} diff --git a/apps/mobile/src/app/(tabs)/shell/[connectionId]/[channelId].tsx b/apps/mobile/src/app/(tabs)/shell/[connectionId]/[channelId].tsx index 65a8f7b..e828cc0 100644 --- a/apps/mobile/src/app/(tabs)/shell/[connectionId]/[channelId].tsx +++ b/apps/mobile/src/app/(tabs)/shell/[connectionId]/[channelId].tsx @@ -1,11 +1,5 @@ import { RnRussh } from '@fressh/react-native-uniffi-russh'; -import { - Link, - Stack, - useLocalSearchParams, - useNavigation, - useRouter, -} from 'expo-router'; +import { Stack, useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useRef, useState } from 'react'; import { Platform, @@ -17,7 +11,7 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useTheme } from '@/theme'; +import { useTheme } from '@/lib/theme'; export default function TabsShellDetail() { return ; diff --git a/apps/mobile/src/app/(tabs)/shell/index.tsx b/apps/mobile/src/app/(tabs)/shell/index.tsx index 5838d0d..46e97fc 100644 --- a/apps/mobile/src/app/(tabs)/shell/index.tsx +++ b/apps/mobile/src/app/(tabs)/shell/index.tsx @@ -8,15 +8,22 @@ import { useQuery } from '@tanstack/react-query'; import { Link } from 'expo-router'; import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { listSshShellsQueryOptions } from '@/lib/query-fns'; +import { useTheme ,type AppTheme } from '@/lib/theme'; export default function TabsShellList() { - return ; + const theme = useTheme(); + return ( + + + + ); } type ShellWithConnection = SshShellSession & { connection: SshConnection }; -function ShellList() { +function ShellContent() { const connectionsWithShells = useQuery(listSshShellsQueryOptions); if (!connectionsWithShells.data) { @@ -26,6 +33,8 @@ function ShellList() { } function LoadingState() { + const theme = useTheme(); + const styles = React.useMemo(() => makeStyles(theme), [theme]); return ( Loading... @@ -67,6 +76,8 @@ function LoadedState({ } function EmptyState() { + const theme = useTheme(); + const styles = React.useMemo(() => makeStyles(theme), [theme]); return ( No active shells. Connect from Host tab. @@ -76,6 +87,8 @@ function EmptyState() { } function ShellCard({ shell }: { shell: ShellWithConnection }) { + const theme = useTheme(); + const styles = React.useMemo(() => makeStyles(theme), [theme]); return ( {shell.connectionId} @@ -93,7 +106,9 @@ function ShellCard({ shell }: { shell: ShellWithConnection }) { ); } -const styles = StyleSheet.create({ - container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, - text: { color: 'black', marginBottom: 8 }, -}); +function makeStyles(theme: AppTheme) { + return StyleSheet.create({ + container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, + text: { color: theme.colors.textPrimary, marginBottom: 8 }, + }); +} diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 62bc354..9bf354d 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -2,8 +2,8 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { isLiquidGlassAvailable } from 'expo-glass-effect'; import { Stack } from 'expo-router'; import React from 'react'; +import { ThemeProvider } from '../lib/theme'; import { queryClient } from '../lib/utils'; -import { ThemeProvider } from '../theme'; console.log('Fressh App Init', { isLiquidGlassAvailable: isLiquidGlassAvailable(), diff --git a/apps/mobile/src/theme/index.tsx b/apps/mobile/src/lib/theme.tsx similarity index 81% rename from apps/mobile/src/theme/index.tsx rename to apps/mobile/src/lib/theme.tsx index 893512c..1a89ac7 100644 --- a/apps/mobile/src/theme/index.tsx +++ b/apps/mobile/src/lib/theme.tsx @@ -6,12 +6,18 @@ export type AppTheme = { surface: string; terminalBackground: string; border: string; + borderStrong: string; textPrimary: string; textSecondary: string; muted: string; primary: string; buttonTextOnPrimary: string; inputBackground: string; + danger: string; + overlay: string; + transparent: string; + shadow: string; + primaryDisabled: string; }; }; @@ -21,12 +27,18 @@ export const darkTheme: AppTheme = { surface: '#111B34', terminalBackground: '#0E172B', border: '#2A3655', + borderStrong: '#1E293B', textPrimary: '#E5E7EB', textSecondary: '#C6CBD3', muted: '#9AA0A6', primary: '#2563EB', buttonTextOnPrimary: '#FFFFFF', inputBackground: '#0E172B', + danger: '#FCA5A5', + overlay: 'rgba(0,0,0,0.4)', + transparent: 'transparent', + shadow: '#000000', + primaryDisabled: '#3B82F6', }, };