diff --git a/mobile/src/app/(tabs)/capture.tsx b/mobile/src/app/(tabs)/capture.tsx index 5bfd6f6..d8b695b 100644 --- a/mobile/src/app/(tabs)/capture.tsx +++ b/mobile/src/app/(tabs)/capture.tsx @@ -6,6 +6,7 @@ import { useNotesStore, type NotesState } from '../../store/notes-store'; import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store'; import { useIntakeStore, type IntakeState } from '../../store/intake-store'; import { submitIntake, type IntakeSubmitResult } from '../../api/intake'; +import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility'; import { colors } from '../../theme'; type CaptureMode = 'text' | 'photo' | 'voice' | 'url' | 'scan' | 'paste'; @@ -133,7 +134,7 @@ export default function CaptureScreen() { const isActive = workspace.id === activeWorkspaceId; return ( setActiveWorkspace(workspace.id)} style={[styles.workspaceChip, isActive ? styles.workspaceChipActive : null]} @@ -153,7 +154,7 @@ export default function CaptureScreen() { return ( { setMode(m); resetForm(); }} style={[styles.modeCard, isActive ? styles.modeCardActive : null]} > @@ -167,28 +168,28 @@ export default function CaptureScreen() { {/* Mode-specific content */} {mode === 'text' && ( <> - - + + )} {mode === 'url' && ( <> - { setUrlInput(t); setIntakeResult(null); }} placeholder="https://example.com/article" placeholderTextColor={colors.textTertiary} style={styles.input} autoCapitalize="none" keyboardType="url" /> + { setUrlInput(t); setIntakeResult(null); }} placeholder="https://example.com/article" placeholderTextColor={colors.textTertiary} style={styles.input} autoCapitalize="none" keyboardType="url" /> {intakeResult ? ( Processing as {intakeResult.contentType}… Job: {intakeResult.jobId} ) : ( - void handleUrlIntake()} disabled={busy || !urlInput.trim()} style={[styles.button, busy ? styles.buttonDisabled : null]}> - {busy ? 'Submitting...' : 'Process with AI'} + void handleUrlIntake()} disabled={busy || !urlInput.trim()} style={[styles.button, (busy || !urlInput.trim()) ? styles.buttonDisabled : null]}> + {busy ? 'Submitting...' : 'Process with AI'} )} {body ? ( <> - - + + ) : null} @@ -196,13 +197,13 @@ export default function CaptureScreen() { {mode === 'paste' && ( <> - - {busy ? 'Reading clipboard...' : 'Paste & Clean'} + + {busy ? 'Reading clipboard...' : 'Paste & Clean'} {body ? ( <> - - + + ) : null} @@ -212,8 +213,8 @@ export default function CaptureScreen() { Voice-to-Note Record audio and transcribe to text. Requires expo-av for audio recording. - - Start Recording + + Start Recording )} @@ -222,8 +223,8 @@ export default function CaptureScreen() { Screenshot-to-Note Take a photo or select from gallery. Uses vision AI for OCR and text extraction. - - Open Camera + + Open Camera )} @@ -232,8 +233,8 @@ export default function CaptureScreen() { Document Scan Photograph multiple pages of a document. Each page is processed with vision AI and combined into a single note. - - Start Scanning + + Start Scanning )} @@ -244,12 +245,12 @@ export default function CaptureScreen() { {/* Save button (shown when we have content to save) */} {(mode === 'text' || body) && ( - {busy ? 'Saving...' : activeWorkspaceId ? 'Save draft' : 'Select workspace'} + {busy ? 'Saving...' : activeWorkspaceId ? 'Save draft' : 'Select workspace'} )} {saved ? Draft saved to the product backend. : null} diff --git a/mobile/src/app/(tabs)/capture/scan.tsx b/mobile/src/app/(tabs)/capture/scan.tsx index 186ca28..000e822 100644 --- a/mobile/src/app/(tabs)/capture/scan.tsx +++ b/mobile/src/app/(tabs)/capture/scan.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; import { useRouter } from 'expo-router'; +import { buttonA11y, dynamicType } from '../../../lib/accessibility'; import { colors } from '../../../theme'; export default function ScanCaptureScreen() { @@ -13,9 +14,9 @@ export default function ScanCaptureScreen() { Use your camera to scan a multi-page document. Text will be extracted via OCR. setScanning(!scanning)} - accessibilityLabel={scanning ? 'Stop scanning' : 'Start scanning'} > {scanning ? '⏹' : '📄'} {scanning ? 'Stop' : 'Scan Document'} @@ -25,8 +26,8 @@ export default function ScanCaptureScreen() { Camera-based document scanning will be available in a future release. For now, use the Text or URL capture modes. - router.back()}> - Go Back + router.back()}> + Go Back ); diff --git a/mobile/src/app/(tabs)/capture/url.tsx b/mobile/src/app/(tabs)/capture/url.tsx index ec13e74..b4a43c4 100644 --- a/mobile/src/app/(tabs)/capture/url.tsx +++ b/mobile/src/app/(tabs)/capture/url.tsx @@ -4,6 +4,7 @@ import { useRouter } from 'expo-router'; import { submitIntake, type IntakeSubmitResult } from '../../../api/intake'; import { useWorkspaceStore, type WorkspaceState } from '../../../store/workspace-store'; import { useIntakeStore, type IntakeState } from '../../../store/intake-store'; +import { buttonA11y, textInputA11y, dynamicType } from '../../../lib/accessibility'; import { colors } from '../../../theme'; function classifyUrlLocal(url: string): string { @@ -63,7 +64,7 @@ export default function UrlCaptureScreen() { onChangeText={(text) => { setUrl(text); setResult(null); setError(null); }} autoCapitalize="none" keyboardType="url" - accessibilityLabel="URL input" + {...textInputA11y('URL input')} /> {detectedType && ( @@ -86,24 +87,24 @@ export default function UrlCaptureScreen() { ) : ( void handleProcess()} - accessibilityLabel="Process URL with AI" > {busy ? ( ) : ( - Process with AI + Process with AI )} - Options… + Options… )} diff --git a/mobile/src/app/(tabs)/capture/voice.tsx b/mobile/src/app/(tabs)/capture/voice.tsx index b31e38f..84dcf07 100644 --- a/mobile/src/app/(tabs)/capture/voice.tsx +++ b/mobile/src/app/(tabs)/capture/voice.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; import { useRouter } from 'expo-router'; +import { buttonA11y, dynamicType } from '../../../lib/accessibility'; import { colors } from '../../../theme'; export default function VoiceCaptureScreen() { @@ -14,9 +15,9 @@ export default function VoiceCaptureScreen() { Tap the microphone to start recording. Speech will be transcribed automatically. setRecording(!recording)} - accessibilityLabel={recording ? 'Stop recording' : 'Start recording'} > {recording ? '⏹' : '🎙'} {recording ? 'Stop' : 'Record'} @@ -27,8 +28,8 @@ export default function VoiceCaptureScreen() { {transcript} - router.back()}> - Save as Note + router.back()}> + Save as Note )} diff --git a/mobile/src/app/(tabs)/inbox.tsx b/mobile/src/app/(tabs)/inbox.tsx index 8645e30..590a807 100644 --- a/mobile/src/app/(tabs)/inbox.tsx +++ b/mobile/src/app/(tabs)/inbox.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { useInboxStore, type ActivityItem, type ApprovalItem, type InboxState } from '../../store/inbox-store'; +import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility'; import { colors } from '../../theme'; export default function InboxScreen() { @@ -24,6 +25,7 @@ export default function InboxScreen() { {isLoading ? Loading approvals… : null} {approvals.length > 0 ? ( Status: {item.status} - + {pendingApprovalId === item.id ? 'Approving…' : 'Approve'} - + {pendingApprovalId === item.id ? 'Rejecting…' : 'Reject'} diff --git a/mobile/src/app/(tabs)/index.tsx b/mobile/src/app/(tabs)/index.tsx index e6d5753..0449f02 100644 --- a/mobile/src/app/(tabs)/index.tsx +++ b/mobile/src/app/(tabs)/index.tsx @@ -5,6 +5,7 @@ import type { MobileNote } from '../../api/notes'; import { useNotesStore, type NotesState } from '../../store/notes-store'; import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store'; import type { MobileWorkspace } from '../../api/workspaces'; +import { buttonA11y } from '../../lib/accessibility'; import { colors } from '../../theme'; export default function HomeScreen() { @@ -41,7 +42,7 @@ export default function HomeScreen() { setActiveWorkspace(null)} style={[styles.workspaceChip, activeWorkspaceId === null ? styles.workspaceChipActive : null]} > @@ -54,7 +55,7 @@ export default function HomeScreen() { return ( setActiveWorkspace(workspace.id)} style={[styles.workspaceChip, isActive ? styles.workspaceChipActive : null]} @@ -69,7 +70,7 @@ export default function HomeScreen() { {isLoading ? Loading notes… : null} {recentNotes.map((note: (typeof recentNotes)[number]) => ( - router.push(`/note/${note.id}`)} style={styles.card}> + router.push(`/note/${note.id}`)} style={styles.card}> {note.workspace} diff --git a/mobile/src/app/(tabs)/search.tsx b/mobile/src/app/(tabs)/search.tsx index ea5f1dd..d333fa1 100644 --- a/mobile/src/app/(tabs)/search.tsx +++ b/mobile/src/app/(tabs)/search.tsx @@ -5,6 +5,7 @@ import type { MobileNote } from '../../api/notes'; import type { MobileWorkspace } from '../../api/workspaces'; import { useNotesStore, type NotesState } from '../../store/notes-store'; import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store'; +import { buttonA11y, textInputA11y } from '../../lib/accessibility'; import { colors } from '../../theme'; export default function SearchScreen() { @@ -39,7 +40,7 @@ export default function SearchScreen() { Search Scope: {activeWorkspaceName ?? 'All workspaces'} {isLoading ? Loading searchable notes… : null} {results.map((note: MobileNote) => ( - router.push(`/note/${note.id}`)} style={styles.row}> + router.push(`/note/${note.id}`)} style={styles.row}> {note.title} ))} diff --git a/mobile/src/app/(tabs)/settings.tsx b/mobile/src/app/(tabs)/settings.tsx index 49b8a6f..c764126 100644 --- a/mobile/src/app/(tabs)/settings.tsx +++ b/mobile/src/app/(tabs)/settings.tsx @@ -4,6 +4,7 @@ import { router } from 'expo-router'; import { useAuthStore, type AuthState } from '../../store/auth-store'; import { getFeedbackClient } from '../../lib/feedback-client'; import { APP_PLATFORM } from '../../lib/app-metadata'; +import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility'; import { colors } from '../../theme'; type FeedbackType = 'bug' | 'feature' | 'praise' | 'other'; @@ -50,14 +51,14 @@ export default function SettingsScreen() { Signed in as {email ?? 'Unknown account'} { signOut(); router.replace('/auth'); }} > - Sign out + Sign out @@ -68,7 +69,7 @@ export default function SettingsScreen() { const isActive = type === feedbackType; return ( setFeedbackType(type)} @@ -80,6 +81,7 @@ export default function SettingsScreen() { { void submitFeedback(); }} > - {isSubmitting ? 'Submitting…' : 'Submit feedback'} + {isSubmitting ? 'Submitting…' : 'Submit feedback'} diff --git a/mobile/src/app/_layout.tsx b/mobile/src/app/_layout.tsx index 7435207..2bebaef 100644 --- a/mobile/src/app/_layout.tsx +++ b/mobile/src/app/_layout.tsx @@ -13,6 +13,7 @@ import { getSurveyClient } from '../lib/survey-client'; import { flushNoteQueue, getNoteQueueSize } from '../lib/offline-queue'; import type { InAppMessage } from '@bytelyst/broadcast-client'; import type { ActiveSurvey, Question, QuestionAnswer } from '@bytelyst/survey-client'; +import { buttonA11y, textInputA11y, dynamicType } from '../lib/accessibility'; import { colors } from '../theme'; export default function RootLayout() { @@ -268,8 +269,8 @@ export default function RootLayout() { {message.body ? {message.body} : null} {message.dismissible !== false ? ( - void dismissBroadcast(message.id)}> - × + void dismissBroadcast(message.id)}> + × ) : null} @@ -289,11 +290,11 @@ export default function RootLayout() { {activeSurvey.title} - void startSurvey()}> - Start + void startSurvey()}> + Start - void dismissSurvey()}> - Dismiss + void dismissSurvey()}> + Dismiss @@ -322,10 +323,11 @@ export default function RootLayout() { {currentQuestion.options.map((option) => ( void submitAnswer(option.id)} > - {option.emoji ? `${option.emoji} ` : ''}{option.text} + {option.emoji ? `${option.emoji} ` : ''}{option.text} ))} @@ -335,14 +337,15 @@ export default function RootLayout() { { length: (currentQuestion.maxValue ?? 5) - (currentQuestion.minValue ?? 1) + 1 }, (_, index) => (currentQuestion.minValue ?? 1) + index, ).map((value) => ( - void submitAnswer(String(value))}> - {value} + void submitAnswer(String(value))}> + {value} ))} ) : ( <> void submitAnswer(textAnswer.trim())} > - Next + Next )} - void dismissSurvey()}> - Close + void dismissSurvey()}> + Close diff --git a/mobile/src/app/auth.tsx b/mobile/src/app/auth.tsx index c8bbb98..9952beb 100644 --- a/mobile/src/app/auth.tsx +++ b/mobile/src/app/auth.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { router } from 'expo-router'; import { useAuthStore, type AuthState } from '../store/auth-store'; +import { buttonA11y, textInputA11y, dynamicType } from '../lib/accessibility'; import { colors } from '../theme'; export default function AuthScreen() { @@ -27,6 +28,7 @@ export default function AuthScreen() { {mode === 'signin' ? 'Sign in to continue' : 'Create your account'} {mode === 'register' ? ( { setErrorMessage(null); @@ -39,6 +41,7 @@ export default function AuthScreen() { /> ) : null} { @@ -51,6 +54,7 @@ export default function AuthScreen() { value={email} /> { setErrorMessage(null); setPassword(value); @@ -63,7 +67,8 @@ export default function AuthScreen() { /> {errorMessage ? {errorMessage} : null} { const didAuthenticate = mode === 'signin' @@ -81,21 +86,21 @@ export default function AuthScreen() { : 'Registration failed. Check your details or connection and try again.', ); }} - style={styles.button} + style={[styles.button, isLoading ? styles.buttonDisabled : null]} > - + {isLoading ? (mode === 'signin' ? 'Signing in…' : 'Creating account…') : mode === 'signin' ? 'Sign in' : 'Register'} { setErrorMessage(null); setMode((current) => (current === 'signin' ? 'register' : 'signin')); }} style={styles.modeSwitch} > - + {mode === 'signin' ? 'Need an account? Register' : 'Already have an account? Sign in'} @@ -137,6 +142,9 @@ const styles = StyleSheet.create({ paddingVertical: 14, alignItems: 'center', }, + buttonDisabled: { + opacity: 0.55, + }, buttonText: { color: colors.textPrimary, fontWeight: '700', diff --git a/mobile/src/app/intake.tsx b/mobile/src/app/intake.tsx index ddca6da..ec90951 100644 --- a/mobile/src/app/intake.tsx +++ b/mobile/src/app/intake.tsx @@ -6,6 +6,7 @@ import { listPromptTemplates, type MobilePromptTemplate } from '../api/note-prom import { useWorkspaceStore, type WorkspaceState } from '../store/workspace-store'; import { useIntakeStore, type IntakeState } from '../store/intake-store'; import type { MobileWorkspace } from '../api/workspaces'; +import { buttonA11y, dynamicType } from '../lib/accessibility'; import { colors } from '../theme'; type ContentTypeBadge = { label: string; color: string }; @@ -157,7 +158,7 @@ export default function IntakeScreen() { Template setSelectedTemplate(null)} style={[styles.chip, selectedTemplate === null ? styles.chipActive : null]} > @@ -168,7 +169,7 @@ export default function IntakeScreen() { {templates.slice(0, 8).map((t) => ( setSelectedTemplate(t.slug)} style={[styles.chip, selectedTemplate === t.slug ? styles.chipActive : null]} > @@ -198,7 +199,7 @@ export default function IntakeScreen() { {/* Process button */} {!result && ( void handleProcess()} disabled={busy || !incomingUrl.trim()} style={[styles.button, (busy || !incomingUrl.trim()) ? styles.buttonDisabled : null]} @@ -206,7 +207,7 @@ export default function IntakeScreen() { {busy ? ( ) : ( - Process + Process )} )} diff --git a/mobile/src/app/note/[id].tsx b/mobile/src/app/note/[id].tsx index b693bd4..c2fa980 100644 --- a/mobile/src/app/note/[id].tsx +++ b/mobile/src/app/note/[id].tsx @@ -4,6 +4,7 @@ import { ActivityIndicator, Pressable, ScrollView, Share, StyleSheet, Text, Text import { useNotesStore, type NotesState } from '../../store/notes-store'; import { usePromptStore, type PromptState } from '../../store/prompt-store'; import { getReadingTime, suggestTags } from '../../api/note-prompts'; +import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility'; import { colors } from '../../theme'; export default function NoteDetailScreen() { @@ -53,6 +54,7 @@ export default function NoteDetailScreen() { {isEditing ? ( <> { setIsEditing(false); @@ -78,25 +81,25 @@ export default function NoteDetailScreen() { setDraftBody(selectedNote?.body ?? ''); }} > - Cancel + Cancel { await updateNote(noteId, draftTitle, draftBody); setIsEditing(false); }} > - Save + Save ) : !isLoading ? ( <> {selectedNote?.body ?? 'No note body is available yet.'} - setIsEditing(true)}> - Edit note + setIsEditing(true)}> + Edit note ) : null} @@ -115,7 +118,7 @@ export default function NoteDetailScreen() { Share { void Share.share({ @@ -124,10 +127,10 @@ export default function NoteDetailScreen() { }); }} > - Share as text + Share as text { void Share.share({ @@ -136,7 +139,7 @@ export default function NoteDetailScreen() { }); }} > - Share link + Share link @@ -145,7 +148,7 @@ export default function NoteDetailScreen() { {/* Smart Actions */} { setShowSmartActions(!showSmartActions); @@ -170,7 +173,7 @@ export default function NoteDetailScreen() { )} { if (!selectedNote) return; @@ -179,7 +182,7 @@ export default function NoteDetailScreen() { .catch(() => {}); }} > - Suggest tags + Suggest tags {suggestedTags.length > 0 && ( @@ -195,7 +198,7 @@ export default function NoteDetailScreen() { {templates.slice(0, 6).map((t) => ( { @@ -207,7 +210,7 @@ export default function NoteDetailScreen() { }); }} > - {t.name} + {t.name} {t.description} ))} @@ -228,8 +231,8 @@ export default function NoteDetailScreen() { {lastResult.model} · {lastResult.usage?.totalTokens ?? 0} tokens )} - - Dismiss + + Dismiss )} diff --git a/mobile/src/app/prompt-result.tsx b/mobile/src/app/prompt-result.tsx index 11729bc..2f65a23 100644 --- a/mobile/src/app/prompt-result.tsx +++ b/mobile/src/app/prompt-result.tsx @@ -1,6 +1,7 @@ import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { usePromptStore } from '../store/prompt-store'; +import { buttonA11y, dynamicType } from '../lib/accessibility'; import { colors } from '../theme'; export default function PromptResultScreen() { @@ -12,8 +13,8 @@ export default function PromptResultScreen() { return ( No prompt result available. - router.back()}> - Go Back + router.back()}> + Go Back ); @@ -45,13 +46,14 @@ export default function PromptResultScreen() { { clearResult(); router.back(); }} > - Dismiss + Dismiss diff --git a/mobile/src/lib/accessibility.ts b/mobile/src/lib/accessibility.ts new file mode 100644 index 0000000..e5617ee --- /dev/null +++ b/mobile/src/lib/accessibility.ts @@ -0,0 +1,39 @@ +import type { AccessibilityState } from 'react-native'; + +export const dynamicType = { + control: 1.4, + body: 1.8, + heading: 1.5, +} as const; + +type ButtonA11yOptions = { + disabled?: boolean; + expanded?: boolean; + selected?: boolean; +}; + +export function buttonA11y(label: string, options: ButtonA11yOptions = {}) { + const accessibilityState: AccessibilityState = {}; + + if (options.disabled !== undefined) { + accessibilityState.disabled = options.disabled; + } + if (options.expanded !== undefined) { + accessibilityState.expanded = options.expanded; + } + if (options.selected !== undefined) { + accessibilityState.selected = options.selected; + } + + return { + accessibilityLabel: label, + accessibilityRole: 'button' as const, + ...(Object.keys(accessibilityState).length > 0 ? { accessibilityState } : {}), + }; +} + +export function textInputA11y(label: string) { + return { + accessibilityLabel: label, + }; +}