feat: add Expo GitHub mobile app (React Native port)
Port of SwiftUI iOS app to Expo/React Native for cross-platform support. - Session list with repository info and status - Chat view with collapsible tool executions - Message input with model/repo/branch selection chips - PR creation flow simulation - Dark theme matching Claude Code aesthetic - KeyboardAvoidingView for proper keyboard handling https://claude.ai/code/session_018qmTB96S8xvNRQFrhcvzJp
This commit is contained in:
parent
cffaf0b904
commit
9ddebad13c
41
apps/expo-github-mobile/.gitignore
vendored
Normal file
41
apps/expo-github-mobile/.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
web-build/
|
||||||
|
expo-env.d.ts
|
||||||
|
|
||||||
|
# Native
|
||||||
|
.kotlin/
|
||||||
|
*.orig.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
|
||||||
|
# Metro
|
||||||
|
.metro-health-check*
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.*
|
||||||
|
yarn-debug.*
|
||||||
|
yarn-error.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# generated native folders
|
||||||
|
/ios
|
||||||
|
/android
|
||||||
32
apps/expo-github-mobile/App.tsx
Normal file
32
apps/expo-github-mobile/App.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { StatusBar } from 'expo-status-bar'
|
||||||
|
import { AppProvider } from './src/contexts/AppContext'
|
||||||
|
import { Session } from './src/models/types'
|
||||||
|
import SessionListView from './src/views/SessionListView'
|
||||||
|
import ChatView from './src/views/ChatView'
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [currentView, setCurrentView] = useState<'list' | 'chat'>('list')
|
||||||
|
const [activeSession, setActiveSession] = useState<Session | null>(null)
|
||||||
|
|
||||||
|
const handleSessionPress = (session: Session) => {
|
||||||
|
setActiveSession(session)
|
||||||
|
setCurrentView('chat')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBackToList = () => {
|
||||||
|
setCurrentView('list')
|
||||||
|
setActiveSession(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppProvider>
|
||||||
|
<StatusBar style="light" />
|
||||||
|
{currentView === 'list' ? (
|
||||||
|
<SessionListView onSessionPress={handleSessionPress} />
|
||||||
|
) : activeSession ? (
|
||||||
|
<ChatView session={activeSession} onBack={handleBackToList} />
|
||||||
|
) : null}
|
||||||
|
</AppProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
84
apps/expo-github-mobile/README.md
Normal file
84
apps/expo-github-mobile/README.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# GitHub Mobile App - Expo
|
||||||
|
|
||||||
|
A React Native mobile app inspired by Claude Code, built with Expo. This is a port of the SwiftUI iOS app to React Native/Expo for cross-platform support.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Session list view with repository info and status
|
||||||
|
- Chat view with collapsible tool executions (Write, Bash, Read, Edit, Glob, Grep)
|
||||||
|
- Message input with model/repo/branch selection chips
|
||||||
|
- PR creation flow simulation
|
||||||
|
- Dark theme matching Claude Code aesthetic
|
||||||
|
- Mock data for repos, sessions, and tool calls
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- React Native with Expo SDK 54
|
||||||
|
- TypeScript
|
||||||
|
- React Context for state management
|
||||||
|
- Expo Vector Icons (Ionicons)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # Reusable components
|
||||||
|
│ ├── MessageInputView.tsx
|
||||||
|
│ ├── SessionRow.tsx
|
||||||
|
│ ├── MessageView.tsx
|
||||||
|
│ ├── ToolCallView.tsx
|
||||||
|
│ ├── Chip.tsx
|
||||||
|
│ └── NewSessionSheet.tsx
|
||||||
|
├── contexts/ # React Context providers
|
||||||
|
│ └── AppContext.tsx
|
||||||
|
├── models/ # TypeScript types and data
|
||||||
|
│ └── types.ts
|
||||||
|
├── theme/ # Colors, spacing, styling
|
||||||
|
│ ├── colors.ts
|
||||||
|
│ └── types.ts
|
||||||
|
└── views/ # Screen components
|
||||||
|
├── SessionListView.tsx
|
||||||
|
└── ChatView.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
npx expo start
|
||||||
|
|
||||||
|
# Run on iOS simulator
|
||||||
|
npm run ios
|
||||||
|
|
||||||
|
# Run on Android emulator
|
||||||
|
npm run android
|
||||||
|
|
||||||
|
# Run on web
|
||||||
|
npm run web
|
||||||
|
```
|
||||||
|
|
||||||
|
## Colors & Theme
|
||||||
|
|
||||||
|
The app uses a dark theme matching Claude Code's aesthetic:
|
||||||
|
- Background: `#141414`
|
||||||
|
- Card: `#292929`
|
||||||
|
- Accent (warm orange): `#D98C33`
|
||||||
|
- Tool colors: Write (green), Bash (blue), Read (purple)
|
||||||
|
|
||||||
|
## Models & Data
|
||||||
|
|
||||||
|
Mock data includes:
|
||||||
|
- 6 repositories (clawdbot, systemss, Awesome-Prompts, etc.)
|
||||||
|
- 7 sessions with various titles
|
||||||
|
- Active session with tool call examples (Write, Bash commands)
|
||||||
|
|
||||||
|
## Port Notes
|
||||||
|
|
||||||
|
This is a direct port of the SwiftUI app at `../ios-github-mobile/`. Key differences:
|
||||||
|
- Uses React Native components instead of SwiftUI
|
||||||
|
- Ionicons instead of SF Symbols
|
||||||
|
- React Context instead of `@Observable`
|
||||||
|
- Modal-based sheets instead of SwiftUI sheets
|
||||||
30
apps/expo-github-mobile/app.json
Normal file
30
apps/expo-github-mobile/app.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "expo-github-mobile",
|
||||||
|
"slug": "expo-github-mobile",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"userInterfaceStyle": "light",
|
||||||
|
"newArchEnabled": true,
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/splash-icon.png",
|
||||||
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"edgeToEdgeEnabled": true,
|
||||||
|
"predictiveBackGestureEnabled": false
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/expo-github-mobile/assets/adaptive-icon.png
Normal file
BIN
apps/expo-github-mobile/assets/adaptive-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/expo-github-mobile/assets/favicon.png
Normal file
BIN
apps/expo-github-mobile/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
apps/expo-github-mobile/assets/icon.png
Normal file
BIN
apps/expo-github-mobile/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/expo-github-mobile/assets/splash-icon.png
Normal file
BIN
apps/expo-github-mobile/assets/splash-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
8
apps/expo-github-mobile/index.ts
Normal file
8
apps/expo-github-mobile/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { registerRootComponent } from 'expo';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||||
|
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||||
|
// the environment is set up appropriately
|
||||||
|
registerRootComponent(App);
|
||||||
9119
apps/expo-github-mobile/package-lock.json
generated
Normal file
9119
apps/expo-github-mobile/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
apps/expo-github-mobile/package.json
Normal file
27
apps/expo-github-mobile/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "expo-github-mobile",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"start": "expo start",
|
||||||
|
"android": "expo start --android",
|
||||||
|
"ios": "expo start --ios",
|
||||||
|
"web": "expo start --web"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/vector-icons": "^15.0.3",
|
||||||
|
"@react-navigation/native": "^7.1.28",
|
||||||
|
"@react-navigation/native-stack": "^7.10.1",
|
||||||
|
"expo": "~54.0.32",
|
||||||
|
"expo-status-bar": "~3.0.9",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-native": "0.81.5",
|
||||||
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
|
"react-native-screens": "~4.16.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "~19.1.0",
|
||||||
|
"typescript": "~5.9.2"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
50
apps/expo-github-mobile/src/components/Chip.tsx
Normal file
50
apps/expo-github-mobile/src/components/Chip.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View, Text, StyleSheet } from 'react-native'
|
||||||
|
import { Ionicons } from '@expo/vector-icons'
|
||||||
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon?: string
|
||||||
|
text: string
|
||||||
|
secondaryText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Chip: React.FC<Props> = ({ icon, text, secondaryText }) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{icon && <Ionicons name={icon as any} size={16} color={Colors.primaryText} />}
|
||||||
|
{secondaryText ? (
|
||||||
|
<>
|
||||||
|
<Text style={styles.secondaryLabel}>{text}</Text>
|
||||||
|
<Text style={styles.text}>{secondaryText}</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.text}>{text}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: Spacing.XS,
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
backgroundColor: Colors.chipBackground,
|
||||||
|
borderRadius: Radius.Full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Colors.chipBorder,
|
||||||
|
},
|
||||||
|
secondaryLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Chip
|
||||||
67
apps/expo-github-mobile/src/components/MessageInputView.tsx
Normal file
67
apps/expo-github-mobile/src/components/MessageInputView.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native'
|
||||||
|
import { Ionicons } from '@expo/vector-icons'
|
||||||
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string
|
||||||
|
placeholder: string
|
||||||
|
onChangeText: (text: string) => void
|
||||||
|
onSend: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageInputView: React.FC<Props> = ({ text, placeholder, onChangeText, onSend }) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.inputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={text}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={Colors.secondaryText}
|
||||||
|
multiline
|
||||||
|
maxLength={2000}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.sendButton, { opacity: text.trim() ? 1 : 0.5 }]}
|
||||||
|
onPress={onSend}
|
||||||
|
disabled={!text.trim()}
|
||||||
|
>
|
||||||
|
<Ionicons name="arrow-up" size={20} color={Colors.buttonPrimaryText} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
},
|
||||||
|
inputWrapper: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
backgroundColor: Colors.inputBackground,
|
||||||
|
borderRadius: Radius.LG,
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
gap: Spacing.SM,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
maxHeight: 120,
|
||||||
|
},
|
||||||
|
sendButton: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: Colors.buttonPrimary,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MessageInputView
|
||||||
57
apps/expo-github-mobile/src/components/MessageView.tsx
Normal file
57
apps/expo-github-mobile/src/components/MessageView.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { View, Text, ScrollView, StyleSheet } from 'react-native'
|
||||||
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
|
import { Message } from '../models/types'
|
||||||
|
import ToolCallView from './ToolCallView'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message: Message
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageView: React.FC<Props> = ({ message }) => {
|
||||||
|
const [expandedToolCalls, setExpandedToolCalls] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
|
const toggleToolCall = (id: string) => {
|
||||||
|
setExpandedToolCalls((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id)
|
||||||
|
} else {
|
||||||
|
next.add(id)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Message content */}
|
||||||
|
{message.content && (
|
||||||
|
<Text style={styles.messageContent}>{message.content}</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tool calls */}
|
||||||
|
{message.toolCalls.map((toolCall) => (
|
||||||
|
<ToolCallView
|
||||||
|
key={toolCall.id}
|
||||||
|
toolCall={toolCall}
|
||||||
|
isExpanded={expandedToolCalls.has(toolCall.id)}
|
||||||
|
onToggle={() => toggleToolCall(toolCall.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
gap: Spacing.MD,
|
||||||
|
},
|
||||||
|
messageContent: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
paddingHorizontal: Spacing.LG,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MessageView
|
||||||
280
apps/expo-github-mobile/src/components/NewSessionSheet.tsx
Normal file
280
apps/expo-github-mobile/src/components/NewSessionSheet.tsx
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
Modal,
|
||||||
|
TouchableOpacity,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
SafeAreaView,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native'
|
||||||
|
import { Ionicons } from '@expo/vector-icons'
|
||||||
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
|
import { useAppState } from '../contexts/AppContext'
|
||||||
|
import {
|
||||||
|
Repository,
|
||||||
|
Session,
|
||||||
|
AIModel,
|
||||||
|
AIModels,
|
||||||
|
allModels,
|
||||||
|
} from '../models/types'
|
||||||
|
import MessageInputView from './MessageInputView'
|
||||||
|
import Chip from './Chip'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSessionCreate: (session: Session) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const NewSessionSheet: React.FC<Props> = ({ visible, onClose, onSessionCreate }) => {
|
||||||
|
const { repositories, createNewSession, sendMessage } = useAppState()
|
||||||
|
const [messageText, setMessageText] = useState('')
|
||||||
|
const [selectedRepository, setSelectedRepository] = useState<Repository | undefined>()
|
||||||
|
const [selectedModel, setSelectedModel] = useState<AIModel>(AIModels.sonnet)
|
||||||
|
const [showModelPicker, setShowModelPicker] = useState(false)
|
||||||
|
const [showRepoPicker, setShowRepoPicker] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && repositories.length > 0 && !selectedRepository) {
|
||||||
|
setSelectedRepository(repositories[0])
|
||||||
|
}
|
||||||
|
}, [visible, repositories])
|
||||||
|
|
||||||
|
const handleCreateSession = () => {
|
||||||
|
if (!messageText.trim() || !selectedRepository) return
|
||||||
|
|
||||||
|
const session = createNewSession(messageText.slice(0, 50), selectedRepository)
|
||||||
|
sendMessage(messageText, session)
|
||||||
|
setMessageText('')
|
||||||
|
onSessionCreate(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal visible={visible} animationType="slide" presentationStyle="pageSheet">
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={onClose}>
|
||||||
|
<Text style={styles.cancelButton}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>New Session</Text>
|
||||||
|
<View style={{ width: 50 }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content with keyboard avoidance */}
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.content}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
|
keyboardVerticalOffset={0}
|
||||||
|
>
|
||||||
|
<View style={{ flex: 1 }} />
|
||||||
|
|
||||||
|
{/* Message input area */}
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<MessageInputView
|
||||||
|
text={messageText}
|
||||||
|
placeholder="Ask Claude to help with code..."
|
||||||
|
onChangeText={setMessageText}
|
||||||
|
onSend={handleCreateSession}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Bottom chips */}
|
||||||
|
<View style={styles.chipsContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.chip}
|
||||||
|
onPress={() => setShowModelPicker(true)}
|
||||||
|
>
|
||||||
|
<Ionicons name="cube-outline" size={16} color={Colors.primaryText} />
|
||||||
|
<Text style={styles.chipText}>{selectedModel.displayName}</Text>
|
||||||
|
<Ionicons name="chevron-down" size={12} color={Colors.primaryText} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.chip}
|
||||||
|
onPress={() => setShowRepoPicker(true)}
|
||||||
|
>
|
||||||
|
<Ionicons name="folder-outline" size={16} color={Colors.primaryText} />
|
||||||
|
<Text style={styles.chipText}>
|
||||||
|
{selectedRepository?.name || 'Select repo'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{selectedRepository && (
|
||||||
|
<View style={styles.chip}>
|
||||||
|
<Ionicons name="git-branch-outline" size={16} color={Colors.primaryText} />
|
||||||
|
<Text style={styles.chipText}>{selectedRepository.defaultBranch}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
|
{/* Model Picker Modal */}
|
||||||
|
<PickerModal
|
||||||
|
visible={showModelPicker}
|
||||||
|
onClose={() => setShowModelPicker(false)}
|
||||||
|
title="Select Model"
|
||||||
|
>
|
||||||
|
{allModels.map((model) => (
|
||||||
|
<PickerItem
|
||||||
|
key={model.id}
|
||||||
|
title={model.displayName}
|
||||||
|
selected={selectedModel.id === model.id}
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedModel(model)
|
||||||
|
setShowModelPicker(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</PickerModal>
|
||||||
|
|
||||||
|
{/* Repository Picker Modal */}
|
||||||
|
<PickerModal
|
||||||
|
visible={showRepoPicker}
|
||||||
|
onClose={() => setShowRepoPicker(false)}
|
||||||
|
title="Select Repository"
|
||||||
|
>
|
||||||
|
{repositories.map((repo) => (
|
||||||
|
<PickerItem
|
||||||
|
key={repo.id}
|
||||||
|
title={repo.name}
|
||||||
|
subtitle={`${repo.owner}/${repo.name}`}
|
||||||
|
selected={selectedRepository?.id === repo.id}
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedRepository(repo)
|
||||||
|
setShowRepoPicker(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</PickerModal>
|
||||||
|
</SafeAreaView>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Picker Modal Component
|
||||||
|
const PickerModal: React.FC<{
|
||||||
|
visible: boolean
|
||||||
|
onClose: () => void
|
||||||
|
title: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}> = ({ visible, onClose, title, children }) => (
|
||||||
|
<Modal visible={visible} animationType="slide" presentationStyle="pageSheet">
|
||||||
|
<SafeAreaView style={styles.pickerContainer}>
|
||||||
|
<View style={styles.pickerHeader}>
|
||||||
|
<TouchableOpacity onPress={onClose}>
|
||||||
|
<Text style={styles.cancelButton}>Done</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>{title}</Text>
|
||||||
|
<View style={{ width: 40 }} />
|
||||||
|
</View>
|
||||||
|
<ScrollView>{children}</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Picker Item Component
|
||||||
|
const PickerItem: React.FC<{
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
selected: boolean
|
||||||
|
onPress: () => void
|
||||||
|
}> = ({ title, subtitle, selected, onPress }) => (
|
||||||
|
<TouchableOpacity style={styles.pickerItem} onPress={onPress}>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.pickerItemTitle}>{title}</Text>
|
||||||
|
{subtitle && <Text style={styles.pickerItemSubtitle}>{subtitle}</Text>}
|
||||||
|
</View>
|
||||||
|
{selected && <Ionicons name="checkmark" size={20} color={Colors.accent} />}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: Colors.background,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: Spacing.LG,
|
||||||
|
paddingVertical: Spacing.MD,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: Colors.surfaceBackground,
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
paddingBottom: Spacing.LG,
|
||||||
|
},
|
||||||
|
chipsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: Spacing.SM,
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingTop: Spacing.MD,
|
||||||
|
},
|
||||||
|
chip: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: Spacing.XS,
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
backgroundColor: Colors.chipBackground,
|
||||||
|
borderRadius: Radius.Full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Colors.chipBorder,
|
||||||
|
},
|
||||||
|
chipText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
pickerContainer: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: Colors.background,
|
||||||
|
},
|
||||||
|
pickerHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: Spacing.LG,
|
||||||
|
paddingVertical: Spacing.MD,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: Colors.surfaceBackground,
|
||||||
|
},
|
||||||
|
pickerItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: Spacing.LG,
|
||||||
|
paddingVertical: Spacing.MD,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderBottomColor: Colors.surfaceBackground,
|
||||||
|
},
|
||||||
|
pickerItemTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
pickerItemSubtitle: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default NewSessionSheet
|
||||||
47
apps/expo-github-mobile/src/components/SessionRow.tsx
Normal file
47
apps/expo-github-mobile/src/components/SessionRow.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
|
||||||
|
import { Ionicons } from '@expo/vector-icons'
|
||||||
|
import { Colors, Spacing } from '../theme/colors'
|
||||||
|
import { Session, getFullName } from '../models/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
session: Session
|
||||||
|
onPress: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SessionRow: React.FC<Props> = ({ session, onPress }) => {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity style={styles.container} onPress={onPress}>
|
||||||
|
<View style={styles.textContainer}>
|
||||||
|
<Text style={styles.title} numberOfLines={2}>
|
||||||
|
{session.title}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.repoName}>{getFullName(session.repository)}</Text>
|
||||||
|
</View>
|
||||||
|
<Ionicons name="chevron-forward" size={16} color={Colors.tertiaryText} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: Spacing.LG,
|
||||||
|
paddingVertical: Spacing.MD,
|
||||||
|
},
|
||||||
|
textContainer: {
|
||||||
|
flex: 1,
|
||||||
|
gap: Spacing.XS,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
repoName: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default SessionRow
|
||||||
81
apps/expo-github-mobile/src/components/ToolCallView.tsx
Normal file
81
apps/expo-github-mobile/src/components/ToolCallView.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native'
|
||||||
|
import { Ionicons } from '@expo/vector-icons'
|
||||||
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
|
import { ToolCall } from '../models/types'
|
||||||
|
import { getToolColor } from '../theme/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
toolCall: ToolCall
|
||||||
|
isExpanded: boolean
|
||||||
|
onToggle: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolCallView: React.FC<Props> = ({ toolCall, isExpanded, onToggle }) => {
|
||||||
|
const toolColor = getToolColor(toolCall.type)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<TouchableOpacity style={styles.header} onPress={onToggle}>
|
||||||
|
<Text style={[styles.toolType, { color: toolColor }]}>{toolCall.type}</Text>
|
||||||
|
<Text style={styles.toolInput} numberOfLines={1}>
|
||||||
|
{toolCall.input}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name={isExpanded ? 'chevron-up' : 'chevron-down'}
|
||||||
|
size={12}
|
||||||
|
color={Colors.tertiaryText}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Expanded content */}
|
||||||
|
{isExpanded && toolCall.output && (
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
style={styles.outputContainer}
|
||||||
|
>
|
||||||
|
<Text style={styles.outputText}>{toolCall.output}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: Colors.cardBackground,
|
||||||
|
borderRadius: Radius.MD,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginHorizontal: Spacing.LG,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: Spacing.SM,
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
},
|
||||||
|
toolType: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
toolInput: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
},
|
||||||
|
outputContainer: {
|
||||||
|
backgroundColor: Colors.surfaceBackground,
|
||||||
|
maxHeight: 200,
|
||||||
|
},
|
||||||
|
outputText: {
|
||||||
|
fontFamily: 'Menlo',
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
padding: Spacing.MD,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ToolCallView
|
||||||
104
apps/expo-github-mobile/src/contexts/AppContext.tsx
Normal file
104
apps/expo-github-mobile/src/contexts/AppContext.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'
|
||||||
|
import {
|
||||||
|
Session,
|
||||||
|
Repository,
|
||||||
|
Message,
|
||||||
|
MessageRole,
|
||||||
|
AIModel,
|
||||||
|
mockSessions,
|
||||||
|
mockRepositories,
|
||||||
|
mockActiveSession,
|
||||||
|
SessionStatus,
|
||||||
|
} from '../models/types'
|
||||||
|
|
||||||
|
interface AppContextType {
|
||||||
|
sessions: Session[]
|
||||||
|
repositories: Repository[]
|
||||||
|
activeSession: Session | null
|
||||||
|
openSession: (session: Session) => void
|
||||||
|
closeSession: () => void
|
||||||
|
createNewSession: (title: string, repository: Repository) => Session
|
||||||
|
sendMessage: (content: string, toSession: Session) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppContext = createContext<AppContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const [sessions, setSessions] = useState<Session[]>([mockActiveSession, ...mockSessions])
|
||||||
|
const [repositories] = useState<Repository[]>(mockRepositories)
|
||||||
|
const [activeSession, setActiveSession] = useState<Session | null>(null)
|
||||||
|
|
||||||
|
const openSession = useCallback((session: Session) => {
|
||||||
|
setActiveSession(session)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const closeSession = useCallback(() => {
|
||||||
|
setActiveSession(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const createNewSession = useCallback(
|
||||||
|
(title: string, repository: Repository): Session => {
|
||||||
|
const newSession: Session = {
|
||||||
|
id: `session-${Date.now()}`,
|
||||||
|
title,
|
||||||
|
repository,
|
||||||
|
createdAt: new Date(),
|
||||||
|
status: SessionStatus.Running,
|
||||||
|
messages: [],
|
||||||
|
}
|
||||||
|
setSessions((prev) => [newSession, ...prev])
|
||||||
|
return newSession
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const sendMessage = useCallback((content: string, toSession: Session) => {
|
||||||
|
const newMessage: Message = {
|
||||||
|
id: `msg-${Date.now()}`,
|
||||||
|
role: MessageRole.User,
|
||||||
|
content,
|
||||||
|
timestamp: new Date(),
|
||||||
|
toolCalls: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessions((prev) =>
|
||||||
|
prev.map((s) => {
|
||||||
|
if (s.id === toSession.id) {
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
messages: [...s.messages, newMessage],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider
|
||||||
|
value={{
|
||||||
|
sessions,
|
||||||
|
repositories,
|
||||||
|
activeSession,
|
||||||
|
openSession,
|
||||||
|
closeSession,
|
||||||
|
createNewSession,
|
||||||
|
sendMessage,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AppContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppState = () => {
|
||||||
|
const context = useContext(AppContext)
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAppState must be used within an AppProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export types for convenience
|
||||||
|
export type { Session, Repository, Message, AIModel }
|
||||||
|
export { MessageRole, SessionStatus, ToolType, ToolCallStatus } from '../models/types'
|
||||||
229
apps/expo-github-mobile/src/models/types.ts
Normal file
229
apps/expo-github-mobile/src/models/types.ts
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
// MARK: - Repository
|
||||||
|
|
||||||
|
export interface Repository {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
owner: string
|
||||||
|
defaultBranch: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFullName = (repo: Repository) => `${repo.owner}/${repo.name}`
|
||||||
|
|
||||||
|
export const mockRepositories: Repository[] = [
|
||||||
|
{ id: '1', name: 'clawdbot', owner: 'semihpolat', defaultBranch: 'main' },
|
||||||
|
{ id: '2', name: 'systemss', owner: 'semihpolat', defaultBranch: 'main' },
|
||||||
|
{ id: '3', name: 'Awesome-Prompts', owner: 'semihpolat', defaultBranch: 'main' },
|
||||||
|
{ id: '4', name: 'statue', owner: 'semihpolat', defaultBranch: 'main' },
|
||||||
|
{ id: '5', name: 'littleagents', owner: 'semihpolat', defaultBranch: 'main' },
|
||||||
|
{ id: '6', name: 'ultimateaiscraper', owner: 'semihpolat', defaultBranch: 'main' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Session
|
||||||
|
|
||||||
|
export enum SessionStatus {
|
||||||
|
Idle = 'Idle',
|
||||||
|
Running = 'Running',
|
||||||
|
Completed = 'Completed',
|
||||||
|
Error = 'Error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
repository: Repository
|
||||||
|
createdAt: Date
|
||||||
|
status: SessionStatus
|
||||||
|
messages: Message[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Message
|
||||||
|
|
||||||
|
export enum MessageRole {
|
||||||
|
User = 'user',
|
||||||
|
Assistant = 'assistant',
|
||||||
|
System = 'system',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
id: string
|
||||||
|
role: MessageRole
|
||||||
|
content: string
|
||||||
|
timestamp: Date
|
||||||
|
toolCalls: ToolCall[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tool Call
|
||||||
|
|
||||||
|
export enum ToolType {
|
||||||
|
Write = 'Write',
|
||||||
|
Bash = 'Bash',
|
||||||
|
Read = 'Read',
|
||||||
|
Edit = 'Edit',
|
||||||
|
Glob = 'Glob',
|
||||||
|
Grep = 'Grep',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ToolCallStatus {
|
||||||
|
Pending = 'pending',
|
||||||
|
Running = 'running',
|
||||||
|
Completed = 'completed',
|
||||||
|
Failed = 'failed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolCall {
|
||||||
|
id: string
|
||||||
|
type: ToolType
|
||||||
|
name: string
|
||||||
|
input: string
|
||||||
|
output?: string
|
||||||
|
status: ToolCallStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AI Model
|
||||||
|
|
||||||
|
export interface AIModel {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
displayName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AIModels = {
|
||||||
|
sonnet: { id: 'sonnet-4.5', name: 'sonnet', displayName: 'Sonnet 4.5' } as AIModel,
|
||||||
|
opus: { id: 'opus-4.5', name: 'opus', displayName: 'Opus 4.5' } as AIModel,
|
||||||
|
haiku: { id: 'haiku', name: 'haiku', displayName: 'Haiku' } as AIModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const allModels = [AIModels.sonnet, AIModels.opus, AIModels.haiku]
|
||||||
|
|
||||||
|
// MARK: - Mock Data
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
export const mockSessions: Session[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Design Claude Code system architecture for mobile',
|
||||||
|
repository: mockRepositories[1],
|
||||||
|
createdAt: new Date(now.getTime() - 3600 * 1000),
|
||||||
|
status: SessionStatus.Idle,
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Add popular tweets with images to platform showcase',
|
||||||
|
repository: mockRepositories[2],
|
||||||
|
createdAt: new Date(now.getTime() - 7200 * 1000),
|
||||||
|
status: SessionStatus.Idle,
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Add popular tweets with images to platform library',
|
||||||
|
repository: mockRepositories[2],
|
||||||
|
createdAt: new Date(now.getTime() - 10800 * 1000),
|
||||||
|
status: SessionStatus.Idle,
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: 'Evaluate migrating SvelteKit project to Next.js',
|
||||||
|
repository: mockRepositories[3],
|
||||||
|
createdAt: new Date(now.getTime() - 14400 * 1000),
|
||||||
|
status: SessionStatus.Idle,
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
title: 'Update resources list with current tools and links',
|
||||||
|
repository: mockRepositories[4],
|
||||||
|
createdAt: new Date(now.getTime() - 18000 * 1000),
|
||||||
|
status: SessionStatus.Idle,
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
title: 'Sync fork with original repository',
|
||||||
|
repository: mockRepositories[3],
|
||||||
|
createdAt: new Date(now.getTime() - 21600 * 1000),
|
||||||
|
status: SessionStatus.Idle,
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
title: 'Improve UI Design with Shadcn Style',
|
||||||
|
repository: mockRepositories[5],
|
||||||
|
createdAt: new Date(now.getTime() - 25200 * 1000),
|
||||||
|
status: SessionStatus.Idle,
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const mockActiveSession: Session = {
|
||||||
|
id: 'active-1',
|
||||||
|
title: 'Add popular tweets with images to platform',
|
||||||
|
repository: mockRepositories[2],
|
||||||
|
createdAt: new Date(now.getTime() - 1800 * 1000),
|
||||||
|
status: SessionStatus.Running,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: 'm1',
|
||||||
|
role: MessageRole.Assistant,
|
||||||
|
content: 'Haklisın, hemen README.md\'yi oluşturup push yapıyorum!',
|
||||||
|
timestamp: new Date(now.getTime() - 1200 * 1000),
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id: 'tc1',
|
||||||
|
type: ToolType.Write,
|
||||||
|
name: 'Write',
|
||||||
|
input: '/home/user/Awesome-Prompts/README.md',
|
||||||
|
output: `+ # Awesome AI Prompts
|
||||||
|
+
|
||||||
|
+ A curated collection of powerful AI prompts that went viral on X/Twitter.
|
||||||
|
+ These prompts have been battle-tested by thousands of users and proven to deliver
|
||||||
|
+ exceptional results with ChatGPT, Claude, and other LLMs.
|
||||||
|
+`,
|
||||||
|
status: ToolCallStatus.Completed,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'm2',
|
||||||
|
role: MessageRole.Assistant,
|
||||||
|
content: 'Şimdi commit ve push yapıyorum:',
|
||||||
|
timestamp: new Date(now.getTime() - 900 * 1000),
|
||||||
|
toolCalls: [
|
||||||
|
{
|
||||||
|
id: 'tc2',
|
||||||
|
type: ToolType.Bash,
|
||||||
|
name: 'Bash',
|
||||||
|
input: 'git add README.md && git status',
|
||||||
|
status: ToolCallStatus.Completed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tc3',
|
||||||
|
type: ToolType.Bash,
|
||||||
|
name: 'Bash',
|
||||||
|
input: `git commit -m "$(cat <<'EOF'
|
||||||
|
Add comprehensive Awesome AI Prompts collection...
|
||||||
|
EOF
|
||||||
|
)"`,
|
||||||
|
status: ToolCallStatus.Completed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tc4',
|
||||||
|
type: ToolType.Bash,
|
||||||
|
name: 'Bash',
|
||||||
|
input: 'git push -u origin claude/add-popular-tweets-dy04J',
|
||||||
|
status: ToolCallStatus.Completed,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'm3',
|
||||||
|
role: MessageRole.Assistant,
|
||||||
|
content: 'Push tamam! Şimdi PR oluşturuyorum:',
|
||||||
|
timestamp: new Date(now.getTime() - 600 * 1000),
|
||||||
|
toolCalls: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
53
apps/expo-github-mobile/src/theme/colors.ts
Normal file
53
apps/expo-github-mobile/src/theme/colors.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Theme colors matching SwiftUI AppTheme
|
||||||
|
export const Colors = {
|
||||||
|
// Background colors
|
||||||
|
background: '#141414', // rgb(0.08, 0.08, 0.08)
|
||||||
|
surfaceBackground: '#1E1E1E', // rgb(0.12, 0.12, 0.12)
|
||||||
|
cardBackground: '#292929', // rgb(0.16, 0.16, 0.16)
|
||||||
|
inputBackground: '#242424', // rgb(0.14, 0.14, 0.14)
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
primaryText: '#FFFFFF',
|
||||||
|
secondaryText: '#999999', // rgb(0.6, 0.6, 0.6)
|
||||||
|
tertiaryText: '#737373', // rgb(0.45, 0.45, 0.45)
|
||||||
|
|
||||||
|
// Accent colors
|
||||||
|
accent: '#D98C33', // rgb(0.85, 0.55, 0.35) - Warm orange like Claude
|
||||||
|
accentLight: '#F3C08D', // rgb(0.95, 0.75, 0.55)
|
||||||
|
|
||||||
|
// Status colors
|
||||||
|
success: '#4DB366', // rgb(0.3, 0.7, 0.4)
|
||||||
|
warning: '#E6B34D', // rgb(0.9, 0.7, 0.3)
|
||||||
|
error: '#E66666', // rgb(0.9, 0.4, 0.4)
|
||||||
|
|
||||||
|
// Tool colors
|
||||||
|
toolWrite: '#66B266', // rgb(0.4, 0.7, 0.4)
|
||||||
|
toolBash: '#8099CC', // rgb(0.5, 0.6, 0.8)
|
||||||
|
toolRead: '#B299B2', // rgb(0.7, 0.6, 0.8)
|
||||||
|
|
||||||
|
// Chip/Tag colors
|
||||||
|
chipBackground: '#333333', // rgb(0.2, 0.2, 0.2)
|
||||||
|
chipBorder: '#4D4D4D', // rgb(0.3, 0.3, 0.3)
|
||||||
|
|
||||||
|
// Button colors
|
||||||
|
buttonPrimary: '#F2F2F2', // rgb(0.95, 0.95, 0.95)
|
||||||
|
buttonPrimaryText: '#000000',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Spacing
|
||||||
|
export const Spacing = {
|
||||||
|
XS: 4,
|
||||||
|
SM: 8,
|
||||||
|
MD: 12,
|
||||||
|
LG: 16,
|
||||||
|
XL: 24,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Corner radius
|
||||||
|
export const Radius = {
|
||||||
|
SM: 6,
|
||||||
|
MD: 10,
|
||||||
|
LG: 16,
|
||||||
|
XL: 24,
|
||||||
|
Full: 100,
|
||||||
|
} as const
|
||||||
17
apps/expo-github-mobile/src/theme/types.ts
Normal file
17
apps/expo-github-mobile/src/theme/types.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Helper to get tool color
|
||||||
|
import { Colors } from './colors'
|
||||||
|
import { ToolType } from '../models/types'
|
||||||
|
|
||||||
|
export const getToolColor = (type: ToolType): string => {
|
||||||
|
switch (type) {
|
||||||
|
case ToolType.Write:
|
||||||
|
case ToolType.Edit:
|
||||||
|
return Colors.toolWrite
|
||||||
|
case ToolType.Bash:
|
||||||
|
return Colors.toolBash
|
||||||
|
case ToolType.Read:
|
||||||
|
case ToolType.Glob:
|
||||||
|
case ToolType.Grep:
|
||||||
|
return Colors.toolRead
|
||||||
|
}
|
||||||
|
}
|
||||||
223
apps/expo-github-mobile/src/views/ChatView.tsx
Normal file
223
apps/expo-github-mobile/src/views/ChatView.tsx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
SafeAreaView,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
StyleSheet,
|
||||||
|
} from 'react-native'
|
||||||
|
import { Ionicons } from '@expo/vector-icons'
|
||||||
|
import { useAppState } from '../contexts/AppContext'
|
||||||
|
import { Session, getFullName } from '../models/types'
|
||||||
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
|
import MessageView from '../components/MessageView'
|
||||||
|
import MessageInputView from '../components/MessageInputView'
|
||||||
|
import Chip from '../components/Chip'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
session: Session
|
||||||
|
onBack: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatView: React.FC<Props> = ({ session, onBack }) => {
|
||||||
|
const { sessions, sendMessage } = useAppState()
|
||||||
|
const [messageText, setMessageText] = useState('')
|
||||||
|
const [showCreatePR, setShowCreatePR] = useState(false)
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null)
|
||||||
|
|
||||||
|
// Get current session from state to see updates
|
||||||
|
const currentSession = sessions.find((s) => s.id === session.id) || session
|
||||||
|
|
||||||
|
const hasToolCalls = currentSession.messages.some((m) => m.toolCalls.length > 0)
|
||||||
|
|
||||||
|
const handleSendMessage = () => {
|
||||||
|
if (messageText.trim()) {
|
||||||
|
sendMessage(messageText, currentSession)
|
||||||
|
setMessageText('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto scroll to bottom when messages change
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollViewRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollViewRef.current?.scrollToEnd({ animated: true })
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}, [currentSession.messages.length])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={onBack} style={styles.backButton}>
|
||||||
|
<Ionicons name="chevron-back" size={24} color={Colors.primaryText} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.headerCenter}>
|
||||||
|
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||||
|
{currentSession.title.length > 30
|
||||||
|
? currentSession.title.slice(0, 30) + '...'
|
||||||
|
: currentSession.title}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.headerSubtitle}>
|
||||||
|
{getFullName(currentSession.repository)} · Default
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.menuButton}>
|
||||||
|
<Ionicons name="ellipsis-horizontal-circle" size={24} color={Colors.primaryText} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Messages + Input with keyboard avoidance */}
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.keyboardAvoidingView}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
|
keyboardVerticalOffset={0}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
style={styles.scrollView}
|
||||||
|
contentContainerStyle={styles.messagesContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{currentSession.messages.map((message) => (
|
||||||
|
<MessageView key={message.id} message={message} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Create PR button */}
|
||||||
|
{hasToolCalls && (
|
||||||
|
<View style={styles.prButtonContainer}>
|
||||||
|
<Text style={styles.branchName} numberOfLines={1}>
|
||||||
|
claude/add-popular-tweets-dy04J
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity style={styles.createPRButton} onPress={() => setShowCreatePR(true)}>
|
||||||
|
<Text style={styles.createPRText}>Create PR</Text>
|
||||||
|
<Ionicons name="open-outline" size={14} color={Colors.primaryText} />
|
||||||
|
<Ionicons name="ellipsis-horizontal" size={14} color={Colors.primaryText} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Input area */}
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<MessageInputView
|
||||||
|
text={messageText}
|
||||||
|
placeholder="Add feedback..."
|
||||||
|
onChangeText={setMessageText}
|
||||||
|
onSend={handleSendMessage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Bottom chips */}
|
||||||
|
<View style={styles.chipsContainer}>
|
||||||
|
<Chip icon="code-working" text="Branch" secondaryText="claude/add-popular-tweets..." />
|
||||||
|
<TouchableOpacity style={styles.createPRChip} onPress={() => setShowCreatePR(true)}>
|
||||||
|
<Text style={styles.chipText}>Create PR</Text>
|
||||||
|
<Ionicons name="open-outline" size={12} color={Colors.primaryText} />
|
||||||
|
<Ionicons name="ellipsis-horizontal" size={12} color={Colors.primaryText} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: Colors.background,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
},
|
||||||
|
headerCenter: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: Spacing.SM,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
headerSubtitle: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
},
|
||||||
|
menuButton: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
},
|
||||||
|
keyboardAvoidingView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
messagesContainer: {
|
||||||
|
padding: Spacing.MD,
|
||||||
|
gap: Spacing.LG,
|
||||||
|
},
|
||||||
|
prButtonContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: Colors.cardBackground,
|
||||||
|
borderRadius: Radius.MD,
|
||||||
|
padding: Spacing.MD,
|
||||||
|
marginTop: Spacing.SM,
|
||||||
|
},
|
||||||
|
branchName: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
},
|
||||||
|
createPRButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: Spacing.XS,
|
||||||
|
},
|
||||||
|
createPRText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
backgroundColor: Colors.background,
|
||||||
|
paddingBottom: Spacing.MD,
|
||||||
|
},
|
||||||
|
chipsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: Spacing.SM,
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingTop: Spacing.MD,
|
||||||
|
},
|
||||||
|
createPRChip: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: Spacing.XS,
|
||||||
|
paddingHorizontal: Spacing.MD,
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
backgroundColor: Colors.chipBackground,
|
||||||
|
borderRadius: Radius.MD,
|
||||||
|
},
|
||||||
|
chipText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ChatView
|
||||||
129
apps/expo-github-mobile/src/views/SessionListView.tsx
Normal file
129
apps/expo-github-mobile/src/views/SessionListView.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
|
SafeAreaView,
|
||||||
|
} from 'react-native'
|
||||||
|
import { Ionicons } from '@expo/vector-icons'
|
||||||
|
import { useAppState, Session } from '../contexts/AppContext'
|
||||||
|
import { Colors, Spacing, Radius } from '../theme/colors'
|
||||||
|
import { getFullName } from '../models/types'
|
||||||
|
import SessionRow from '../components/SessionRow'
|
||||||
|
import NewSessionSheet from '../components/NewSessionSheet'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSessionPress: (session: Session) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SessionListView: React.FC<Props> = ({ onSessionPress }) => {
|
||||||
|
const { sessions } = useAppState()
|
||||||
|
const [showNewSessionSheet, setShowNewSessionSheet] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity style={styles.menuButton}>
|
||||||
|
<Ionicons name="ellipsis-horizontal" size={24} color={Colors.primaryText} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>Code</Text>
|
||||||
|
<View style={{ width: 24 }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Status indicator */}
|
||||||
|
<View style={styles.statusContainer}>
|
||||||
|
<Text style={styles.statusText}>Idle</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Session list */}
|
||||||
|
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
||||||
|
{sessions.map((session) => (
|
||||||
|
<SessionRow
|
||||||
|
key={session.id}
|
||||||
|
session={session}
|
||||||
|
onPress={() => {
|
||||||
|
onSessionPress(session)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* New session button */}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.newSessionButton}
|
||||||
|
onPress={() => setShowNewSessionSheet(true)}
|
||||||
|
>
|
||||||
|
<Text style={styles.newSessionButtonText}>New session</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* New session sheet */}
|
||||||
|
<NewSessionSheet
|
||||||
|
visible={showNewSessionSheet}
|
||||||
|
onClose={() => setShowNewSessionSheet(false)}
|
||||||
|
onSessionCreate={(session) => {
|
||||||
|
setShowNewSessionSheet(false)
|
||||||
|
onSessionPress(session)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: Colors.background,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: Spacing.LG,
|
||||||
|
paddingVertical: Spacing.SM,
|
||||||
|
},
|
||||||
|
menuButton: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: Colors.primaryText,
|
||||||
|
},
|
||||||
|
statusContainer: {
|
||||||
|
paddingHorizontal: Spacing.LG,
|
||||||
|
paddingTop: Spacing.MD,
|
||||||
|
paddingBottom: Spacing.SM,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.secondaryText,
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
backgroundColor: Colors.background,
|
||||||
|
paddingHorizontal: Spacing.LG,
|
||||||
|
paddingVertical: Spacing.MD,
|
||||||
|
},
|
||||||
|
newSessionButton: {
|
||||||
|
backgroundColor: Colors.buttonPrimary,
|
||||||
|
borderRadius: Radius.Full,
|
||||||
|
paddingVertical: Spacing.MD,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
newSessionButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: Colors.buttonPrimaryText,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default SessionListView
|
||||||
6
apps/expo-github-mobile/tsconfig.json
Normal file
6
apps/expo-github-mobile/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "expo/tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user