Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/mobile-client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ coverage/

/src/*/*.test.*

/src/*/*.example.*
/src/*/*.example.*
/src/*/*.md
/src/*/*/*.md
22 changes: 15 additions & 7 deletions packages/mobile-client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ThemeProvider,
useTheme,
usePromptHistory,
useScreenReaderAnnouncement,
} from './src/hooks';

// Config
Expand All @@ -44,9 +45,10 @@ import { isInjectPromptResponse, isSyncFullContextMessage } from './src/utils/me
* Main application content with navigation
*/
const AppContent: React.FC = () => {
const { status, socketManager, reconnect } = useConnection();
const { status, error, socketManager, reconnect } = useConnection();
const { theme, isDark } = useTheme();
const { updateHistoryItem } = usePromptHistory();
const { announce } = useScreenReaderAnnouncement();
const [index, setIndex] = useState(0);
const [promptResponse, setPromptResponse] = useState<InjectPromptResponse | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
Expand Down Expand Up @@ -131,11 +133,6 @@ const AppContent: React.FC = () => {
}
};

// Handle response dismissal
const handleResponseDismiss = () => {
setPromptResponse(null);
};

// Handle reconnection (currently unused but kept for future use)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _handleReconnect = async () => {
Expand All @@ -161,11 +158,22 @@ const AppContent: React.FC = () => {
{ key: 'settings', title: 'Settings', focusedIcon: 'cog', unfocusedIcon: 'cog-outline' },
]);

// Announce screen changes for accessibility
// Requirement 14.11: Announce screen changes on navigation
useEffect(() => {
const currentRoute = routes[index];
if (currentRoute) {
announce(`${currentRoute.title} screen`, 300);
}
}, [index, routes, announce]);

// Render scene based on route
const DashboardScene: React.FC = () => (
<View style={styles.scene}>
<Dashboard
connectionStatus={status}
connectionError={error}
onRetry={reconnect}
onNavigateToCompose={() => setIndex(1)}
onNavigateToDiffs={() => setIndex(2)}
onRefresh={async () => {
Expand All @@ -178,7 +186,7 @@ const AppContent: React.FC = () => {
const PromptScene: React.FC = () => (
<View style={styles.scene}>
<PromptComposer onSubmit={handlePromptSubmit} isLoading={isSubmitting} error={promptError} />
<PromptResponseDisplay response={promptResponse} onDismiss={handleResponseDismiss} />
<PromptResponseDisplay response={promptResponse} connectionStatus={status} />
</View>
);

Expand Down
113 changes: 105 additions & 8 deletions packages/mobile-client/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { useDesignSystem } from '../design-system';
import { Text } from '../design-system/typography/Text';
import { Card } from '../design-system/components/Card';
import { Icon } from '../design-system/components/Icon';
import { Button } from '../design-system/components/Button';
import { ProgressBar } from '../design-system/components/ProgressBar';
import { StatusIndicator, ConnectionStatus } from '../design-system/components/StatusIndicator';
import { TopAppBar } from '../navigation/TopAppBar';
import { useLoadingAnnouncement } from '../hooks/useScreenReaderAnnouncement';

/**
* System metrics interface
Expand Down Expand Up @@ -36,6 +38,8 @@ export interface ActivityItem {
*/
export interface DashboardProps {
connectionStatus: ConnectionStatus;
connectionError?: Error | null;
onRetry?: () => void;
metrics?: SystemMetrics;
recentActivity?: ActivityItem[];
onNavigateToDiffs: () => void;
Expand All @@ -59,6 +63,8 @@ export interface DashboardProps {
*/
export const Dashboard: React.FC<DashboardProps> = ({
connectionStatus,
connectionError = null,
onRetry,
metrics = {
uptime: 99.8,
latency: 45,
Expand All @@ -76,6 +82,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
const [isLargeScreen, setIsLargeScreen] = useState(false);
const [refreshing, setRefreshing] = useState(false);

// Announce loading state for accessibility
// Requirement 14.11: Announce loading states
useLoadingAnnouncement(refreshing, 'Refreshing dashboard data', 'Dashboard refreshed');

// Detect screen size for responsive layout (Requirement 13.5)
useEffect(() => {
const updateLayout = () => {
Expand Down Expand Up @@ -174,6 +184,41 @@ export const Dashboard: React.FC<DashboardProps> = ({
{/* Top App Bar (Requirement 3.1) */}
<TopAppBar connectionStatus={connectionStatus} />

{/* Connection Error Banner (Requirement 17.3, 17.9) */}
{connectionError && connectionStatus === 'disconnected' && (
<View
style={[
styles.errorBanner,
{
backgroundColor: `${theme.colors.errorContainer}26`,
borderBottomColor: theme.colors.error,
},
]}
>
<Icon name="error" size={20} color="error" />
<View style={styles.errorContent}>
<Text variant="body-sm" weight="semibold" color="error">
Connection Failed
</Text>
<Text variant="body-sm" color="onSurfaceVariant" style={styles.errorMessage}>
{connectionError.message || 'Unable to connect to relay server'}
</Text>
</View>
{onRetry && (
<Button
variant="tertiary"
size="sm"
onPress={onRetry}
icon={<Icon name="refresh" size={16} color="secondary" />}
accessibilityLabel="Retry connection"
accessibilityHint="Attempts to reconnect to the relay server"
>
Retry
</Button>
)}
</View>
)}

{/* Scrollable Content */}
<ScrollView
style={styles.scrollView}
Expand Down Expand Up @@ -343,6 +388,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
padding="lg"
onPress={onNavigateToDiffs}
style={styles.shortcutCard}
accessibilityLabel="Diff Viewer shortcut"
accessibilityHint="Double tap to navigate to diff viewer screen"
>
<Icon name="compare-arrows" size={32} color="primary" style={styles.shortcutIcon} />
<Text variant="body-md" weight="bold" color="onSurface" style={styles.shortcutTitle}>
Expand All @@ -358,6 +405,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
padding="lg"
onPress={onNavigateToCompose}
style={styles.shortcutCard}
accessibilityLabel="Compose Prompt shortcut"
accessibilityHint="Double tap to navigate to compose prompt screen"
>
<Icon name="terminal" size={32} color="secondary" style={styles.shortcutIcon} />
<Text variant="body-md" weight="bold" color="onSurface" style={styles.shortcutTitle}>
Expand All @@ -370,13 +419,13 @@ export const Dashboard: React.FC<DashboardProps> = ({
</View>
</View>

{/* Recent Activity Section (Requirement 3.7, 15.3) */}
{recentActivity.length > 0 && (
<View style={styles.section}>
<Text variant="headline-md" weight="bold" color="onSurface" style={styles.sectionTitle}>
Recent Activity
</Text>
{/* Recent Activity Section (Requirement 3.7, 15.3, 23.1) */}
<View style={styles.section}>
<Text variant="headline-md" weight="bold" color="onSurface" style={styles.sectionTitle}>
Recent Activity
</Text>

{recentActivity.length > 0 ? (
<Card variant="low" padding="lg" style={styles.activityCard}>
<FlatList
data={recentActivity}
Expand All @@ -390,8 +439,29 @@ export const Dashboard: React.FC<DashboardProps> = ({
)}
/>
</Card>
</View>
)}
) : (
<Card variant="low" padding="lg" style={styles.emptyActivityCard}>
<Icon name="history" size={48} color="onSurfaceVariant" />
<Text
variant="display-lg"
weight="bold"
color="onSurfaceVariant"
align="center"
style={styles.emptyStateHeadline}
>
No Recent Activity
</Text>
<Text
variant="body-md"
color="onSurfaceVariant"
align="center"
style={styles.emptyStateDescription}
>
Your recent commits, syncs, and deployments will appear here
</Text>
</Card>
)}
</View>
</ScrollView>
</View>
);
Expand All @@ -401,6 +471,21 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
errorBanner: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
gap: 12,
borderBottomWidth: 1,
},
errorContent: {
flex: 1,
gap: 4,
},
errorMessage: {
marginTop: 2,
},
scrollView: {
flex: 1,
},
Expand Down Expand Up @@ -501,6 +586,18 @@ const styles = StyleSheet.create({
activityCard: {
minHeight: 200,
},
emptyActivityCard: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 48,
},
emptyStateHeadline: {
marginTop: 16,
},
emptyStateDescription: {
marginTop: 8,
maxWidth: 300,
},
activityItem: {
flexDirection: 'row',
alignItems: 'flex-start',
Expand Down
57 changes: 49 additions & 8 deletions packages/mobile-client/src/components/DiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TopAppBar } from '../navigation/TopAppBar';
import { Card } from '../design-system/components/Card';
import { Button } from '../design-system/components/Button';
import { Icon } from '../design-system/components/Icon';
import { Skeleton } from '../design-system/components/Skeleton';
import SyntaxHighlighter from 'react-syntax-highlighter';

/**
Expand Down Expand Up @@ -76,8 +77,12 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
refreshing={refreshing}
onRefresh={handleRefresh}
tintColor={theme.colors.primary}
accessibilityLabel="Refresh diff changes"
/>
}
accessible={true}
accessibilityLabel="Diff viewer empty state"
accessibilityHint="Pull down to refresh and load diff changes"
>
<Icon name="difference" size={64} color="onSurfaceVariant" />
<Text variant="headline-sm" color="onSurfaceVariant" style={styles.emptyStateText}>
Expand Down Expand Up @@ -411,11 +416,36 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
return (
<View style={[styles.container, { backgroundColor: theme.colors.surface }]}>
<TopAppBar connectionStatus={connectionStatus} />
<View style={styles.loadingContainer}>
<Text variant="body-lg" color="onSurfaceVariant">
Loading...
</Text>
</View>
<ScrollView style={styles.scrollView}>
<View style={{ padding: 16 }}>
{/* File header skeleton */}
<Card variant="low" style={styles.fileHeader}>
<View style={styles.fileHeaderTop}>
<View style={{ flex: 1 }}>
<Skeleton width="60%" height={24} style={{ marginBottom: 8 }} />
<Skeleton width="80%" height={16} />
</View>
</View>
</Card>

{/* Diff content skeleton */}
<Card variant="lowest" style={{ marginTop: 16 }}>
<View style={styles.diffSkeletonContainer}>
{[...Array(10)].map((_, i) => (
<View key={i} style={styles.diffLineSkeletonRow}>
<Skeleton width={40} height={16} style={{ marginRight: 12 }} />
<Skeleton width="85%" height={16} />
</View>
))}
</View>
</Card>

{/* Summary footer skeleton */}
<Card variant="low" style={styles.summaryFooter}>
<Skeleton width="50%" height={20} />
</Card>
</View>
</ScrollView>
</View>
);
}
Expand All @@ -432,8 +462,12 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
refreshing={refreshing}
onRefresh={handleRefresh}
tintColor={theme.colors.primary}
accessibilityLabel="Refresh diff changes"
/>
}
accessible={true}
accessibilityLabel="Diff viewer content"
accessibilityHint="Pull down to refresh diff changes"
>
{/* File Header */}
<Card variant="low" padding="lg" style={styles.fileHeader}>
Expand Down Expand Up @@ -479,6 +513,8 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
size="sm"
onPress={onCommit}
icon={<Icon name="check-circle" size={16} color="onSecondary" />}
accessibilityLabel="Commit changes"
accessibilityHint="Double tap to commit the current changes"
>
Commit
</Button>
Expand All @@ -490,6 +526,8 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
onPress={onRevert}
icon={<Icon name="undo" size={16} color="secondary" />}
style={styles.revertButton}
accessibilityLabel="Revert changes"
accessibilityHint="Double tap to revert the current changes"
>
Revert
</Button>
Expand Down Expand Up @@ -627,10 +665,13 @@ const styles = StyleSheet.create({
scrollView: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
diffSkeletonContainer: {
padding: 16,
},
diffLineSkeletonRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
emptyStateContainer: {
flex: 1,
Expand Down
Loading
Loading