fix(mobile): harden touch accessibility
This commit is contained in:
parent
e0adedc739
commit
f9903fec7c
@ -6,6 +6,7 @@ import { useNotesStore, type NotesState } from '../../store/notes-store';
|
|||||||
import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store';
|
import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store';
|
||||||
import { useIntakeStore, type IntakeState } from '../../store/intake-store';
|
import { useIntakeStore, type IntakeState } from '../../store/intake-store';
|
||||||
import { submitIntake, type IntakeSubmitResult } from '../../api/intake';
|
import { submitIntake, type IntakeSubmitResult } from '../../api/intake';
|
||||||
|
import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility';
|
||||||
import { colors } from '../../theme';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
type CaptureMode = 'text' | 'photo' | 'voice' | 'url' | 'scan' | 'paste';
|
type CaptureMode = 'text' | 'photo' | 'voice' | 'url' | 'scan' | 'paste';
|
||||||
@ -133,7 +134,7 @@ export default function CaptureScreen() {
|
|||||||
const isActive = workspace.id === activeWorkspaceId;
|
const isActive = workspace.id === activeWorkspaceId;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={`Select workspace ${workspace.name}`}
|
{...buttonA11y(`Select workspace ${workspace.name}`, { selected: isActive })}
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
onPress={() => setActiveWorkspace(workspace.id)}
|
onPress={() => setActiveWorkspace(workspace.id)}
|
||||||
style={[styles.workspaceChip, isActive ? styles.workspaceChipActive : null]}
|
style={[styles.workspaceChip, isActive ? styles.workspaceChipActive : null]}
|
||||||
@ -153,7 +154,7 @@ export default function CaptureScreen() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={m}
|
key={m}
|
||||||
accessibilityLabel={`${label} capture mode: ${description}`}
|
{...buttonA11y(`${label} capture mode: ${description}`, { selected: isActive })}
|
||||||
onPress={() => { setMode(m); resetForm(); }}
|
onPress={() => { setMode(m); resetForm(); }}
|
||||||
style={[styles.modeCard, isActive ? styles.modeCardActive : null]}
|
style={[styles.modeCard, isActive ? styles.modeCardActive : null]}
|
||||||
>
|
>
|
||||||
@ -167,28 +168,28 @@ export default function CaptureScreen() {
|
|||||||
{/* Mode-specific content */}
|
{/* Mode-specific content */}
|
||||||
{mode === 'text' && (
|
{mode === 'text' && (
|
||||||
<>
|
<>
|
||||||
<TextInput value={title} onChangeText={setTitle} placeholder="Draft title" placeholderTextColor={colors.textTertiary} style={styles.input} />
|
<TextInput {...textInputA11y('Draft title')} value={title} onChangeText={setTitle} placeholder="Draft title" placeholderTextColor={colors.textTertiary} style={styles.input} />
|
||||||
<TextInput value={body} onChangeText={setBody} placeholder="Capture a thought, task, or note" placeholderTextColor={colors.textTertiary} style={[styles.input, styles.bodyInput]} multiline textAlignVertical="top" />
|
<TextInput {...textInputA11y('Draft body')} value={body} onChangeText={setBody} placeholder="Capture a thought, task, or note" placeholderTextColor={colors.textTertiary} style={[styles.input, styles.bodyInput]} multiline textAlignVertical="top" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mode === 'url' && (
|
{mode === 'url' && (
|
||||||
<>
|
<>
|
||||||
<TextInput value={urlInput} onChangeText={(t) => { setUrlInput(t); setIntakeResult(null); }} placeholder="https://example.com/article" placeholderTextColor={colors.textTertiary} style={styles.input} autoCapitalize="none" keyboardType="url" />
|
<TextInput {...textInputA11y('URL to process')} value={urlInput} onChangeText={(t) => { setUrlInput(t); setIntakeResult(null); }} placeholder="https://example.com/article" placeholderTextColor={colors.textTertiary} style={styles.input} autoCapitalize="none" keyboardType="url" />
|
||||||
{intakeResult ? (
|
{intakeResult ? (
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>Processing as {intakeResult.contentType}…</Text>
|
<Text style={styles.cardTitle}>Processing as {intakeResult.contentType}…</Text>
|
||||||
<Text style={styles.cardBody}>Job: {intakeResult.jobId}</Text>
|
<Text style={styles.cardBody}>Job: {intakeResult.jobId}</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Pressable accessibilityLabel="Process URL with AI" onPress={() => void handleUrlIntake()} disabled={busy || !urlInput.trim()} style={[styles.button, busy ? styles.buttonDisabled : null]}>
|
<Pressable {...buttonA11y('Process URL with AI', { disabled: busy || !urlInput.trim() })} onPress={() => void handleUrlIntake()} disabled={busy || !urlInput.trim()} style={[styles.button, (busy || !urlInput.trim()) ? styles.buttonDisabled : null]}>
|
||||||
<Text style={styles.buttonText}>{busy ? 'Submitting...' : 'Process with AI'}</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.buttonText}>{busy ? 'Submitting...' : 'Process with AI'}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
{body ? (
|
{body ? (
|
||||||
<>
|
<>
|
||||||
<TextInput value={title} onChangeText={setTitle} placeholder="Title" placeholderTextColor={colors.textTertiary} style={styles.input} />
|
<TextInput {...textInputA11y('Processed note title')} value={title} onChangeText={setTitle} placeholder="Title" placeholderTextColor={colors.textTertiary} style={styles.input} />
|
||||||
<TextInput value={body} onChangeText={setBody} placeholder="Processed content" placeholderTextColor={colors.textTertiary} style={[styles.input, styles.bodyInput]} multiline textAlignVertical="top" />
|
<TextInput {...textInputA11y('Processed note body')} value={body} onChangeText={setBody} placeholder="Processed content" placeholderTextColor={colors.textTertiary} style={[styles.input, styles.bodyInput]} multiline textAlignVertical="top" />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
@ -196,13 +197,13 @@ export default function CaptureScreen() {
|
|||||||
|
|
||||||
{mode === 'paste' && (
|
{mode === 'paste' && (
|
||||||
<>
|
<>
|
||||||
<Pressable accessibilityLabel="Read clipboard and clean text" onPress={handlePasteAndClean} disabled={busy} style={[styles.button, busy ? styles.buttonDisabled : null]}>
|
<Pressable {...buttonA11y('Read clipboard and clean text', { disabled: busy })} onPress={handlePasteAndClean} disabled={busy} style={[styles.button, busy ? styles.buttonDisabled : null]}>
|
||||||
<Text style={styles.buttonText}>{busy ? 'Reading clipboard...' : 'Paste & Clean'}</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.buttonText}>{busy ? 'Reading clipboard...' : 'Paste & Clean'}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{body ? (
|
{body ? (
|
||||||
<>
|
<>
|
||||||
<TextInput value={title} onChangeText={setTitle} placeholder="Title" placeholderTextColor={colors.textTertiary} style={styles.input} />
|
<TextInput {...textInputA11y('Pasted note title')} value={title} onChangeText={setTitle} placeholder="Title" placeholderTextColor={colors.textTertiary} style={styles.input} />
|
||||||
<TextInput value={body} onChangeText={setBody} placeholder="Cleaned content" placeholderTextColor={colors.textTertiary} style={[styles.input, styles.bodyInput]} multiline textAlignVertical="top" />
|
<TextInput {...textInputA11y('Pasted note body')} value={body} onChangeText={setBody} placeholder="Cleaned content" placeholderTextColor={colors.textTertiary} style={[styles.input, styles.bodyInput]} multiline textAlignVertical="top" />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
@ -212,8 +213,8 @@ export default function CaptureScreen() {
|
|||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>Voice-to-Note</Text>
|
<Text style={styles.cardTitle}>Voice-to-Note</Text>
|
||||||
<Text style={styles.cardBody}>Record audio and transcribe to text. Requires expo-av for audio recording.</Text>
|
<Text style={styles.cardBody}>Record audio and transcribe to text. Requires expo-av for audio recording.</Text>
|
||||||
<Pressable accessibilityLabel="Start voice recording" onPress={handleVoiceCapture} style={styles.button}>
|
<Pressable {...buttonA11y('Start voice recording')} onPress={handleVoiceCapture} style={styles.button}>
|
||||||
<Text style={styles.buttonText}>Start Recording</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.buttonText}>Start Recording</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@ -222,8 +223,8 @@ export default function CaptureScreen() {
|
|||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>Screenshot-to-Note</Text>
|
<Text style={styles.cardTitle}>Screenshot-to-Note</Text>
|
||||||
<Text style={styles.cardBody}>Take a photo or select from gallery. Uses vision AI for OCR and text extraction.</Text>
|
<Text style={styles.cardBody}>Take a photo or select from gallery. Uses vision AI for OCR and text extraction.</Text>
|
||||||
<Pressable accessibilityLabel="Open camera for photo capture" onPress={handlePhotoCapture} style={styles.button}>
|
<Pressable {...buttonA11y('Open camera for photo capture')} onPress={handlePhotoCapture} style={styles.button}>
|
||||||
<Text style={styles.buttonText}>Open Camera</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.buttonText}>Open Camera</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@ -232,8 +233,8 @@ export default function CaptureScreen() {
|
|||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>Document Scan</Text>
|
<Text style={styles.cardTitle}>Document Scan</Text>
|
||||||
<Text style={styles.cardBody}>Photograph multiple pages of a document. Each page is processed with vision AI and combined into a single note.</Text>
|
<Text style={styles.cardBody}>Photograph multiple pages of a document. Each page is processed with vision AI and combined into a single note.</Text>
|
||||||
<Pressable accessibilityLabel="Start document scan" onPress={handleScanCapture} style={styles.button}>
|
<Pressable {...buttonA11y('Start document scan')} onPress={handleScanCapture} style={styles.button}>
|
||||||
<Text style={styles.buttonText}>Start Scanning</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.buttonText}>Start Scanning</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@ -244,12 +245,12 @@ export default function CaptureScreen() {
|
|||||||
{/* Save button (shown when we have content to save) */}
|
{/* Save button (shown when we have content to save) */}
|
||||||
{(mode === 'text' || body) && (
|
{(mode === 'text' || body) && (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={activeWorkspaceId ? 'Save draft note' : 'Select workspace before saving'}
|
{...buttonA11y(activeWorkspaceId ? 'Save draft note' : 'Select workspace before saving', { disabled: !activeWorkspaceId || busy })}
|
||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
disabled={!activeWorkspaceId || busy}
|
disabled={!activeWorkspaceId || busy}
|
||||||
style={[styles.button, (!activeWorkspaceId || busy) ? styles.buttonDisabled : null]}
|
style={[styles.button, (!activeWorkspaceId || busy) ? styles.buttonDisabled : null]}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>{busy ? 'Saving...' : activeWorkspaceId ? 'Save draft' : 'Select workspace'}</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.buttonText}>{busy ? 'Saving...' : activeWorkspaceId ? 'Save draft' : 'Select workspace'}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
{saved ? <Text style={styles.saved}>Draft saved to the product backend.</Text> : null}
|
{saved ? <Text style={styles.saved}>Draft saved to the product backend.</Text> : null}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
|
import { buttonA11y, dynamicType } from '../../../lib/accessibility';
|
||||||
import { colors } from '../../../theme';
|
import { colors } from '../../../theme';
|
||||||
|
|
||||||
export default function ScanCaptureScreen() {
|
export default function ScanCaptureScreen() {
|
||||||
@ -13,9 +14,9 @@ export default function ScanCaptureScreen() {
|
|||||||
<Text style={styles.hint}>Use your camera to scan a multi-page document. Text will be extracted via OCR.</Text>
|
<Text style={styles.hint}>Use your camera to scan a multi-page document. Text will be extracted via OCR.</Text>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
{...buttonA11y(scanning ? 'Stop scanning' : 'Start scanning', { selected: scanning })}
|
||||||
style={[styles.scanBtn, scanning && styles.scanBtnActive]}
|
style={[styles.scanBtn, scanning && styles.scanBtnActive]}
|
||||||
onPress={() => setScanning(!scanning)}
|
onPress={() => setScanning(!scanning)}
|
||||||
accessibilityLabel={scanning ? 'Stop scanning' : 'Start scanning'}
|
|
||||||
>
|
>
|
||||||
<Text style={styles.scanIcon}>{scanning ? '⏹' : '📄'}</Text>
|
<Text style={styles.scanIcon}>{scanning ? '⏹' : '📄'}</Text>
|
||||||
<Text style={styles.scanLabel}>{scanning ? 'Stop' : 'Scan Document'}</Text>
|
<Text style={styles.scanLabel}>{scanning ? 'Stop' : 'Scan Document'}</Text>
|
||||||
@ -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.
|
Camera-based document scanning will be available in a future release. For now, use the Text or URL capture modes.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
|
<TouchableOpacity {...buttonA11y('Go back')} style={styles.backBtn} onPress={() => router.back()}>
|
||||||
<Text style={styles.backBtnText}>Go Back</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.backBtnText}>Go Back</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useRouter } from 'expo-router';
|
|||||||
import { submitIntake, type IntakeSubmitResult } from '../../../api/intake';
|
import { submitIntake, type IntakeSubmitResult } from '../../../api/intake';
|
||||||
import { useWorkspaceStore, type WorkspaceState } from '../../../store/workspace-store';
|
import { useWorkspaceStore, type WorkspaceState } from '../../../store/workspace-store';
|
||||||
import { useIntakeStore, type IntakeState } from '../../../store/intake-store';
|
import { useIntakeStore, type IntakeState } from '../../../store/intake-store';
|
||||||
|
import { buttonA11y, textInputA11y, dynamicType } from '../../../lib/accessibility';
|
||||||
import { colors } from '../../../theme';
|
import { colors } from '../../../theme';
|
||||||
|
|
||||||
function classifyUrlLocal(url: string): string {
|
function classifyUrlLocal(url: string): string {
|
||||||
@ -63,7 +64,7 @@ export default function UrlCaptureScreen() {
|
|||||||
onChangeText={(text) => { setUrl(text); setResult(null); setError(null); }}
|
onChangeText={(text) => { setUrl(text); setResult(null); setError(null); }}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
keyboardType="url"
|
keyboardType="url"
|
||||||
accessibilityLabel="URL input"
|
{...textInputA11y('URL input')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{detectedType && (
|
{detectedType && (
|
||||||
@ -86,24 +87,24 @@ export default function UrlCaptureScreen() {
|
|||||||
) : (
|
) : (
|
||||||
<View style={styles.buttonRow}>
|
<View style={styles.buttonRow}>
|
||||||
<Pressable
|
<Pressable
|
||||||
|
{...buttonA11y('Process URL with AI', { disabled: busy || !url.trim() })}
|
||||||
style={[styles.btn, (busy || !url.trim()) && styles.btnDisabled]}
|
style={[styles.btn, (busy || !url.trim()) && styles.btnDisabled]}
|
||||||
disabled={busy || !url.trim()}
|
disabled={busy || !url.trim()}
|
||||||
onPress={() => void handleProcess()}
|
onPress={() => void handleProcess()}
|
||||||
accessibilityLabel="Process URL with AI"
|
|
||||||
>
|
>
|
||||||
{busy ? (
|
{busy ? (
|
||||||
<ActivityIndicator color={colors.textPrimary} size="small" />
|
<ActivityIndicator color={colors.textPrimary} size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.btnText}>Process with AI</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.btnText}>Process with AI</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
|
{...buttonA11y('Advanced intake options', { disabled: !url.trim() })}
|
||||||
style={styles.secondaryBtn}
|
style={styles.secondaryBtn}
|
||||||
disabled={!url.trim()}
|
disabled={!url.trim()}
|
||||||
onPress={handleAdvanced}
|
onPress={handleAdvanced}
|
||||||
accessibilityLabel="Advanced intake options"
|
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryBtnText}>Options…</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryBtnText}>Options…</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
|
import { buttonA11y, dynamicType } from '../../../lib/accessibility';
|
||||||
import { colors } from '../../../theme';
|
import { colors } from '../../../theme';
|
||||||
|
|
||||||
export default function VoiceCaptureScreen() {
|
export default function VoiceCaptureScreen() {
|
||||||
@ -14,9 +15,9 @@ export default function VoiceCaptureScreen() {
|
|||||||
<Text style={styles.hint}>Tap the microphone to start recording. Speech will be transcribed automatically.</Text>
|
<Text style={styles.hint}>Tap the microphone to start recording. Speech will be transcribed automatically.</Text>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
{...buttonA11y(recording ? 'Stop recording' : 'Start recording', { selected: recording })}
|
||||||
style={[styles.micBtn, recording && styles.micBtnActive]}
|
style={[styles.micBtn, recording && styles.micBtnActive]}
|
||||||
onPress={() => setRecording(!recording)}
|
onPress={() => setRecording(!recording)}
|
||||||
accessibilityLabel={recording ? 'Stop recording' : 'Start recording'}
|
|
||||||
>
|
>
|
||||||
<Text style={styles.micIcon}>{recording ? '⏹' : '🎙'}</Text>
|
<Text style={styles.micIcon}>{recording ? '⏹' : '🎙'}</Text>
|
||||||
<Text style={styles.micLabel}>{recording ? 'Stop' : 'Record'}</Text>
|
<Text style={styles.micLabel}>{recording ? 'Stop' : 'Record'}</Text>
|
||||||
@ -27,8 +28,8 @@ export default function VoiceCaptureScreen() {
|
|||||||
<View style={styles.transcriptBox}>
|
<View style={styles.transcriptBox}>
|
||||||
<Text style={styles.transcriptText}>{transcript}</Text>
|
<Text style={styles.transcriptText}>{transcript}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={styles.saveBtn} onPress={() => router.back()}>
|
<TouchableOpacity {...buttonA11y('Save transcript as note')} style={styles.saveBtn} onPress={() => router.back()}>
|
||||||
<Text style={styles.saveBtnText}>Save as Note</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.saveBtnText}>Save as Note</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||||
import { useInboxStore, type ActivityItem, type ApprovalItem, type InboxState } from '../../store/inbox-store';
|
import { useInboxStore, type ActivityItem, type ApprovalItem, type InboxState } from '../../store/inbox-store';
|
||||||
|
import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility';
|
||||||
import { colors } from '../../theme';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
export default function InboxScreen() {
|
export default function InboxScreen() {
|
||||||
@ -24,6 +25,7 @@ export default function InboxScreen() {
|
|||||||
{isLoading ? <Text style={styles.empty}>Loading approvals…</Text> : null}
|
{isLoading ? <Text style={styles.empty}>Loading approvals…</Text> : null}
|
||||||
{approvals.length > 0 ? (
|
{approvals.length > 0 ? (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...textInputA11y('Review note')}
|
||||||
style={styles.reviewNoteInput}
|
style={styles.reviewNoteInput}
|
||||||
placeholder="Optional review note…"
|
placeholder="Optional review note…"
|
||||||
placeholderTextColor={colors.textSecondary}
|
placeholderTextColor={colors.textSecondary}
|
||||||
@ -40,7 +42,7 @@ export default function InboxScreen() {
|
|||||||
<Text style={styles.status}>Status: {item.status}</Text>
|
<Text style={styles.status}>Status: {item.status}</Text>
|
||||||
<View style={styles.actionRow}>
|
<View style={styles.actionRow}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={`Approve ${item.title}`}
|
{...buttonA11y(`Approve ${item.title}`, { disabled: pendingApprovalId === item.id || item.status !== 'pending' })}
|
||||||
style={[
|
style={[
|
||||||
styles.approveButton,
|
styles.approveButton,
|
||||||
pendingApprovalId === item.id || item.status !== 'pending' ? styles.buttonDisabled : null,
|
pendingApprovalId === item.id || item.status !== 'pending' ? styles.buttonDisabled : null,
|
||||||
@ -57,12 +59,12 @@ export default function InboxScreen() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.approveText}>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.approveText}>
|
||||||
{pendingApprovalId === item.id ? 'Approving…' : 'Approve'}
|
{pendingApprovalId === item.id ? 'Approving…' : 'Approve'}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={`Reject ${item.title}`}
|
{...buttonA11y(`Reject ${item.title}`, { disabled: pendingApprovalId === item.id || item.status !== 'pending' })}
|
||||||
style={[
|
style={[
|
||||||
styles.rejectButton,
|
styles.rejectButton,
|
||||||
pendingApprovalId === item.id || item.status !== 'pending' ? styles.buttonDisabled : null,
|
pendingApprovalId === item.id || item.status !== 'pending' ? styles.buttonDisabled : null,
|
||||||
@ -79,7 +81,7 @@ export default function InboxScreen() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.rejectText}>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.rejectText}>
|
||||||
{pendingApprovalId === item.id ? 'Rejecting…' : 'Reject'}
|
{pendingApprovalId === item.id ? 'Rejecting…' : 'Reject'}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { MobileNote } from '../../api/notes';
|
|||||||
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
||||||
import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store';
|
import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store';
|
||||||
import type { MobileWorkspace } from '../../api/workspaces';
|
import type { MobileWorkspace } from '../../api/workspaces';
|
||||||
|
import { buttonA11y } from '../../lib/accessibility';
|
||||||
import { colors } from '../../theme';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
@ -41,7 +42,7 @@ export default function HomeScreen() {
|
|||||||
</View>
|
</View>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.workspaceRow}>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.workspaceRow}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Show all workspaces"
|
{...buttonA11y('Show all workspaces', { selected: activeWorkspaceId === null })}
|
||||||
onPress={() => setActiveWorkspace(null)}
|
onPress={() => setActiveWorkspace(null)}
|
||||||
style={[styles.workspaceChip, activeWorkspaceId === null ? styles.workspaceChipActive : null]}
|
style={[styles.workspaceChip, activeWorkspaceId === null ? styles.workspaceChipActive : null]}
|
||||||
>
|
>
|
||||||
@ -54,7 +55,7 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={`Filter by ${workspace.name}`}
|
{...buttonA11y(`Filter by ${workspace.name}`, { selected: isActive })}
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
onPress={() => setActiveWorkspace(workspace.id)}
|
onPress={() => setActiveWorkspace(workspace.id)}
|
||||||
style={[styles.workspaceChip, isActive ? styles.workspaceChipActive : null]}
|
style={[styles.workspaceChip, isActive ? styles.workspaceChipActive : null]}
|
||||||
@ -69,7 +70,7 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
{isLoading ? <Text style={styles.empty}>Loading notes…</Text> : null}
|
{isLoading ? <Text style={styles.empty}>Loading notes…</Text> : null}
|
||||||
{recentNotes.map((note: (typeof recentNotes)[number]) => (
|
{recentNotes.map((note: (typeof recentNotes)[number]) => (
|
||||||
<Pressable accessibilityLabel={`Open note ${note.title}`} key={note.id} onPress={() => router.push(`/note/${note.id}`)} style={styles.card}>
|
<Pressable {...buttonA11y(`Open note ${note.title}`)} key={note.id} onPress={() => router.push(`/note/${note.id}`)} style={styles.card}>
|
||||||
<View style={styles.badgeRow}>
|
<View style={styles.badgeRow}>
|
||||||
<Text style={styles.badge}>{note.workspace}</Text>
|
<Text style={styles.badge}>{note.workspace}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { MobileNote } from '../../api/notes';
|
|||||||
import type { MobileWorkspace } from '../../api/workspaces';
|
import type { MobileWorkspace } from '../../api/workspaces';
|
||||||
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
||||||
import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store';
|
import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store';
|
||||||
|
import { buttonA11y, textInputA11y } from '../../lib/accessibility';
|
||||||
import { colors } from '../../theme';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
export default function SearchScreen() {
|
export default function SearchScreen() {
|
||||||
@ -39,7 +40,7 @@ export default function SearchScreen() {
|
|||||||
<Text style={styles.title}>Search</Text>
|
<Text style={styles.title}>Search</Text>
|
||||||
<Text style={styles.scope}>Scope: {activeWorkspaceName ?? 'All workspaces'}</Text>
|
<Text style={styles.scope}>Scope: {activeWorkspaceName ?? 'All workspaces'}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
accessibilityLabel="Search notes"
|
{...textInputA11y('Search notes')}
|
||||||
value={query}
|
value={query}
|
||||||
onChangeText={setQuery}
|
onChangeText={setQuery}
|
||||||
placeholder="Search notes"
|
placeholder="Search notes"
|
||||||
@ -49,7 +50,7 @@ export default function SearchScreen() {
|
|||||||
<View style={styles.list}>
|
<View style={styles.list}>
|
||||||
{isLoading ? <Text style={styles.empty}>Loading searchable notes…</Text> : null}
|
{isLoading ? <Text style={styles.empty}>Loading searchable notes…</Text> : null}
|
||||||
{results.map((note: MobileNote) => (
|
{results.map((note: MobileNote) => (
|
||||||
<Pressable accessibilityLabel={`Open note ${note.title}`} key={note.id} onPress={() => router.push(`/note/${note.id}`)} style={styles.row}>
|
<Pressable {...buttonA11y(`Open note ${note.title}`)} key={note.id} onPress={() => router.push(`/note/${note.id}`)} style={styles.row}>
|
||||||
<Text style={styles.rowText}>{note.title}</Text>
|
<Text style={styles.rowText}>{note.title}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { router } from 'expo-router';
|
|||||||
import { useAuthStore, type AuthState } from '../../store/auth-store';
|
import { useAuthStore, type AuthState } from '../../store/auth-store';
|
||||||
import { getFeedbackClient } from '../../lib/feedback-client';
|
import { getFeedbackClient } from '../../lib/feedback-client';
|
||||||
import { APP_PLATFORM } from '../../lib/app-metadata';
|
import { APP_PLATFORM } from '../../lib/app-metadata';
|
||||||
|
import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility';
|
||||||
import { colors } from '../../theme';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
type FeedbackType = 'bug' | 'feature' | 'praise' | 'other';
|
type FeedbackType = 'bug' | 'feature' | 'praise' | 'other';
|
||||||
@ -50,14 +51,14 @@ export default function SettingsScreen() {
|
|||||||
<Text style={styles.metaLabel}>Signed in as</Text>
|
<Text style={styles.metaLabel}>Signed in as</Text>
|
||||||
<Text style={styles.metaValue}>{email ?? 'Unknown account'}</Text>
|
<Text style={styles.metaValue}>{email ?? 'Unknown account'}</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Sign out"
|
{...buttonA11y('Sign out')}
|
||||||
style={styles.secondaryButton}
|
style={styles.secondaryButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
signOut();
|
signOut();
|
||||||
router.replace('/auth');
|
router.replace('/auth');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>Sign out</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryButtonText}>Sign out</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@ -68,7 +69,7 @@ export default function SettingsScreen() {
|
|||||||
const isActive = type === feedbackType;
|
const isActive = type === feedbackType;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={`Select ${type} feedback type`}
|
{...buttonA11y(`Select ${type} feedback type`, { selected: isActive })}
|
||||||
key={type}
|
key={type}
|
||||||
style={[styles.typeChip, isActive ? styles.typeChipActive : null]}
|
style={[styles.typeChip, isActive ? styles.typeChipActive : null]}
|
||||||
onPress={() => setFeedbackType(type)}
|
onPress={() => setFeedbackType(type)}
|
||||||
@ -80,6 +81,7 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...textInputA11y('Feedback title')}
|
||||||
value={feedbackTitle}
|
value={feedbackTitle}
|
||||||
onChangeText={setFeedbackTitle}
|
onChangeText={setFeedbackTitle}
|
||||||
placeholder="Feedback title"
|
placeholder="Feedback title"
|
||||||
@ -87,6 +89,7 @@ export default function SettingsScreen() {
|
|||||||
style={styles.input}
|
style={styles.input}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...textInputA11y('Feedback details')}
|
||||||
value={feedbackBody}
|
value={feedbackBody}
|
||||||
onChangeText={setFeedbackBody}
|
onChangeText={setFeedbackBody}
|
||||||
placeholder="Details (optional)"
|
placeholder="Details (optional)"
|
||||||
@ -97,14 +100,14 @@ export default function SettingsScreen() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={isSubmitting ? 'Submitting feedback' : 'Submit feedback'}
|
{...buttonA11y(isSubmitting ? 'Submitting feedback' : 'Submit feedback', { disabled: feedbackTitle.trim().length === 0 || isSubmitting })}
|
||||||
style={[styles.primaryButton, feedbackTitle.trim().length === 0 || isSubmitting ? styles.buttonDisabled : null]}
|
style={[styles.primaryButton, feedbackTitle.trim().length === 0 || isSubmitting ? styles.buttonDisabled : null]}
|
||||||
disabled={feedbackTitle.trim().length === 0 || isSubmitting}
|
disabled={feedbackTitle.trim().length === 0 || isSubmitting}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
void submitFeedback();
|
void submitFeedback();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.primaryButtonText}>{isSubmitting ? 'Submitting…' : 'Submit feedback'}</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.primaryButtonText}>{isSubmitting ? 'Submitting…' : 'Submit feedback'}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { getSurveyClient } from '../lib/survey-client';
|
|||||||
import { flushNoteQueue, getNoteQueueSize } from '../lib/offline-queue';
|
import { flushNoteQueue, getNoteQueueSize } from '../lib/offline-queue';
|
||||||
import type { InAppMessage } from '@bytelyst/broadcast-client';
|
import type { InAppMessage } from '@bytelyst/broadcast-client';
|
||||||
import type { ActiveSurvey, Question, QuestionAnswer } from '@bytelyst/survey-client';
|
import type { ActiveSurvey, Question, QuestionAnswer } from '@bytelyst/survey-client';
|
||||||
|
import { buttonA11y, textInputA11y, dynamicType } from '../lib/accessibility';
|
||||||
import { colors } from '../theme';
|
import { colors } from '../theme';
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
@ -268,8 +269,8 @@ export default function RootLayout() {
|
|||||||
{message.body ? <Text style={styles.bannerBody}>{message.body}</Text> : null}
|
{message.body ? <Text style={styles.bannerBody}>{message.body}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
{message.dismissible !== false ? (
|
{message.dismissible !== false ? (
|
||||||
<Pressable onPress={() => void dismissBroadcast(message.id)}>
|
<Pressable {...buttonA11y(`Dismiss ${message.title}`)} onPress={() => void dismissBroadcast(message.id)}>
|
||||||
<Text style={styles.bannerDismiss}>×</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.bannerDismiss}>×</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
@ -289,11 +290,11 @@ export default function RootLayout() {
|
|||||||
<View style={styles.surveyPrompt}>
|
<View style={styles.surveyPrompt}>
|
||||||
<Text style={styles.surveyPromptText}>{activeSurvey.title}</Text>
|
<Text style={styles.surveyPromptText}>{activeSurvey.title}</Text>
|
||||||
<View style={styles.surveyActions}>
|
<View style={styles.surveyActions}>
|
||||||
<Pressable style={styles.primaryButton} onPress={() => void startSurvey()}>
|
<Pressable {...buttonA11y(`Start survey ${activeSurvey.title}`)} style={styles.primaryButton} onPress={() => void startSurvey()}>
|
||||||
<Text style={styles.primaryButtonText}>Start</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.primaryButtonText}>Start</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable style={styles.secondaryButton} onPress={() => void dismissSurvey()}>
|
<Pressable {...buttonA11y(`Dismiss survey ${activeSurvey.title}`)} style={styles.secondaryButton} onPress={() => void dismissSurvey()}>
|
||||||
<Text style={styles.secondaryButtonText}>Dismiss</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryButtonText}>Dismiss</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -322,10 +323,11 @@ export default function RootLayout() {
|
|||||||
{currentQuestion.options.map((option) => (
|
{currentQuestion.options.map((option) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={option.id}
|
key={option.id}
|
||||||
|
{...buttonA11y(`Answer ${option.text}`)}
|
||||||
style={styles.optionButton}
|
style={styles.optionButton}
|
||||||
onPress={() => void submitAnswer(option.id)}
|
onPress={() => void submitAnswer(option.id)}
|
||||||
>
|
>
|
||||||
<Text style={styles.optionText}>{option.emoji ? `${option.emoji} ` : ''}{option.text}</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.optionText}>{option.emoji ? `${option.emoji} ` : ''}{option.text}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@ -335,14 +337,15 @@ export default function RootLayout() {
|
|||||||
{ length: (currentQuestion.maxValue ?? 5) - (currentQuestion.minValue ?? 1) + 1 },
|
{ length: (currentQuestion.maxValue ?? 5) - (currentQuestion.minValue ?? 1) + 1 },
|
||||||
(_, index) => (currentQuestion.minValue ?? 1) + index,
|
(_, index) => (currentQuestion.minValue ?? 1) + index,
|
||||||
).map((value) => (
|
).map((value) => (
|
||||||
<Pressable key={value} style={styles.ratingButton} onPress={() => void submitAnswer(String(value))}>
|
<Pressable key={value} {...buttonA11y(`Rate ${value}`)} style={styles.ratingButton} onPress={() => void submitAnswer(String(value))}>
|
||||||
<Text style={styles.ratingText}>{value}</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.ratingText}>{value}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...textInputA11y('Survey answer')}
|
||||||
value={textAnswer}
|
value={textAnswer}
|
||||||
onChangeText={setTextAnswer}
|
onChangeText={setTextAnswer}
|
||||||
placeholder="Your answer"
|
placeholder="Your answer"
|
||||||
@ -351,17 +354,18 @@ export default function RootLayout() {
|
|||||||
multiline={currentQuestion?.type === 'text_long'}
|
multiline={currentQuestion?.type === 'text_long'}
|
||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
|
{...buttonA11y('Submit survey answer', { disabled: textAnswer.trim().length === 0 })}
|
||||||
style={[styles.primaryButton, textAnswer.trim().length === 0 ? styles.disabledButton : null]}
|
style={[styles.primaryButton, textAnswer.trim().length === 0 ? styles.disabledButton : null]}
|
||||||
disabled={textAnswer.trim().length === 0}
|
disabled={textAnswer.trim().length === 0}
|
||||||
onPress={() => void submitAnswer(textAnswer.trim())}
|
onPress={() => void submitAnswer(textAnswer.trim())}
|
||||||
>
|
>
|
||||||
<Text style={styles.primaryButtonText}>Next</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.primaryButtonText}>Next</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Pressable style={styles.secondaryButton} onPress={() => void dismissSurvey()}>
|
<Pressable {...buttonA11y('Close survey')} style={styles.secondaryButton} onPress={() => void dismissSurvey()}>
|
||||||
<Text style={styles.secondaryButtonText}>Close</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryButtonText}>Close</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { useAuthStore, type AuthState } from '../store/auth-store';
|
import { useAuthStore, type AuthState } from '../store/auth-store';
|
||||||
|
import { buttonA11y, textInputA11y, dynamicType } from '../lib/accessibility';
|
||||||
import { colors } from '../theme';
|
import { colors } from '../theme';
|
||||||
|
|
||||||
export default function AuthScreen() {
|
export default function AuthScreen() {
|
||||||
@ -27,6 +28,7 @@ export default function AuthScreen() {
|
|||||||
<Text style={styles.subtitle}>{mode === 'signin' ? 'Sign in to continue' : 'Create your account'}</Text>
|
<Text style={styles.subtitle}>{mode === 'signin' ? 'Sign in to continue' : 'Create your account'}</Text>
|
||||||
{mode === 'register' ? (
|
{mode === 'register' ? (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...textInputA11y('Display name')}
|
||||||
autoCapitalize="words"
|
autoCapitalize="words"
|
||||||
onChangeText={(value: string) => {
|
onChangeText={(value: string) => {
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
@ -39,6 +41,7 @@ export default function AuthScreen() {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...textInputA11y('Email address')}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
onChangeText={(value: string) => {
|
onChangeText={(value: string) => {
|
||||||
@ -51,6 +54,7 @@ export default function AuthScreen() {
|
|||||||
value={email}
|
value={email}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...textInputA11y('Password')}
|
||||||
onChangeText={(value: string) => {
|
onChangeText={(value: string) => {
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setPassword(value);
|
setPassword(value);
|
||||||
@ -63,7 +67,8 @@ export default function AuthScreen() {
|
|||||||
/>
|
/>
|
||||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={mode === 'signin' ? 'Sign in' : 'Register account'}
|
{...buttonA11y(mode === 'signin' ? 'Sign in' : 'Register account', { disabled: isLoading })}
|
||||||
|
disabled={isLoading}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
const didAuthenticate =
|
const didAuthenticate =
|
||||||
mode === 'signin'
|
mode === 'signin'
|
||||||
@ -81,21 +86,21 @@ export default function AuthScreen() {
|
|||||||
: 'Registration failed. Check your details or connection and try again.',
|
: 'Registration failed. Check your details or connection and try again.',
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
style={styles.button}
|
style={[styles.button, isLoading ? styles.buttonDisabled : null]}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.buttonText}>
|
||||||
{isLoading ? (mode === 'signin' ? 'Signing in…' : 'Creating account…') : mode === 'signin' ? 'Sign in' : 'Register'}
|
{isLoading ? (mode === 'signin' ? 'Signing in…' : 'Creating account…') : mode === 'signin' ? 'Sign in' : 'Register'}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={mode === 'signin' ? 'Switch to register mode' : 'Switch to sign in mode'}
|
{...buttonA11y(mode === 'signin' ? 'Switch to register mode' : 'Switch to sign in mode')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setMode((current) => (current === 'signin' ? 'register' : 'signin'));
|
setMode((current) => (current === 'signin' ? 'register' : 'signin'));
|
||||||
}}
|
}}
|
||||||
style={styles.modeSwitch}
|
style={styles.modeSwitch}
|
||||||
>
|
>
|
||||||
<Text style={styles.modeSwitchText}>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.modeSwitchText}>
|
||||||
{mode === 'signin' ? 'Need an account? Register' : 'Already have an account? Sign in'}
|
{mode === 'signin' ? 'Need an account? Register' : 'Already have an account? Sign in'}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@ -137,6 +142,9 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
buttonDisabled: {
|
||||||
|
opacity: 0.55,
|
||||||
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: colors.textPrimary,
|
color: colors.textPrimary,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { listPromptTemplates, type MobilePromptTemplate } from '../api/note-prom
|
|||||||
import { useWorkspaceStore, type WorkspaceState } from '../store/workspace-store';
|
import { useWorkspaceStore, type WorkspaceState } from '../store/workspace-store';
|
||||||
import { useIntakeStore, type IntakeState } from '../store/intake-store';
|
import { useIntakeStore, type IntakeState } from '../store/intake-store';
|
||||||
import type { MobileWorkspace } from '../api/workspaces';
|
import type { MobileWorkspace } from '../api/workspaces';
|
||||||
|
import { buttonA11y, dynamicType } from '../lib/accessibility';
|
||||||
import { colors } from '../theme';
|
import { colors } from '../theme';
|
||||||
|
|
||||||
type ContentTypeBadge = { label: string; color: string };
|
type ContentTypeBadge = { label: string; color: string };
|
||||||
@ -157,7 +158,7 @@ export default function IntakeScreen() {
|
|||||||
<Text style={styles.label}>Template</Text>
|
<Text style={styles.label}>Template</Text>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.chipRow}>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.chipRow}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Auto-select template"
|
{...buttonA11y('Auto-select template', { selected: selectedTemplate === null })}
|
||||||
onPress={() => setSelectedTemplate(null)}
|
onPress={() => setSelectedTemplate(null)}
|
||||||
style={[styles.chip, selectedTemplate === null ? styles.chipActive : null]}
|
style={[styles.chip, selectedTemplate === null ? styles.chipActive : null]}
|
||||||
>
|
>
|
||||||
@ -168,7 +169,7 @@ export default function IntakeScreen() {
|
|||||||
{templates.slice(0, 8).map((t) => (
|
{templates.slice(0, 8).map((t) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={t.id}
|
key={t.id}
|
||||||
accessibilityLabel={`Select template ${t.name}`}
|
{...buttonA11y(`Select template ${t.name}`, { selected: selectedTemplate === t.slug })}
|
||||||
onPress={() => setSelectedTemplate(t.slug)}
|
onPress={() => setSelectedTemplate(t.slug)}
|
||||||
style={[styles.chip, selectedTemplate === t.slug ? styles.chipActive : null]}
|
style={[styles.chip, selectedTemplate === t.slug ? styles.chipActive : null]}
|
||||||
>
|
>
|
||||||
@ -198,7 +199,7 @@ export default function IntakeScreen() {
|
|||||||
{/* Process button */}
|
{/* Process button */}
|
||||||
{!result && (
|
{!result && (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Process URL with intake pipeline"
|
{...buttonA11y('Process URL with intake pipeline', { disabled: busy || !incomingUrl.trim() })}
|
||||||
onPress={() => void handleProcess()}
|
onPress={() => void handleProcess()}
|
||||||
disabled={busy || !incomingUrl.trim()}
|
disabled={busy || !incomingUrl.trim()}
|
||||||
style={[styles.button, (busy || !incomingUrl.trim()) ? styles.buttonDisabled : null]}
|
style={[styles.button, (busy || !incomingUrl.trim()) ? styles.buttonDisabled : null]}
|
||||||
@ -206,7 +207,7 @@ export default function IntakeScreen() {
|
|||||||
{busy ? (
|
{busy ? (
|
||||||
<ActivityIndicator color={colors.textPrimary} size="small" />
|
<ActivityIndicator color={colors.textPrimary} size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.buttonText}>Process</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.buttonText}>Process</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ActivityIndicator, Pressable, ScrollView, Share, StyleSheet, Text, Text
|
|||||||
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
||||||
import { usePromptStore, type PromptState } from '../../store/prompt-store';
|
import { usePromptStore, type PromptState } from '../../store/prompt-store';
|
||||||
import { getReadingTime, suggestTags } from '../../api/note-prompts';
|
import { getReadingTime, suggestTags } from '../../api/note-prompts';
|
||||||
|
import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility';
|
||||||
import { colors } from '../../theme';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
export default function NoteDetailScreen() {
|
export default function NoteDetailScreen() {
|
||||||
@ -53,6 +54,7 @@ export default function NoteDetailScreen() {
|
|||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<>
|
<>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...textInputA11y('Note title')}
|
||||||
value={draftTitle}
|
value={draftTitle}
|
||||||
onChangeText={setDraftTitle}
|
onChangeText={setDraftTitle}
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
@ -60,6 +62,7 @@ export default function NoteDetailScreen() {
|
|||||||
placeholderTextColor={colors.textTertiary}
|
placeholderTextColor={colors.textTertiary}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...textInputA11y('Note body')}
|
||||||
value={draftBody}
|
value={draftBody}
|
||||||
onChangeText={setDraftBody}
|
onChangeText={setDraftBody}
|
||||||
style={[styles.input, styles.bodyInput]}
|
style={[styles.input, styles.bodyInput]}
|
||||||
@ -70,7 +73,7 @@ export default function NoteDetailScreen() {
|
|||||||
/>
|
/>
|
||||||
<View style={styles.actionRow}>
|
<View style={styles.actionRow}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Cancel editing"
|
{...buttonA11y('Cancel editing')}
|
||||||
style={styles.secondaryButton}
|
style={styles.secondaryButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
@ -78,25 +81,25 @@ export default function NoteDetailScreen() {
|
|||||||
setDraftBody(selectedNote?.body ?? '');
|
setDraftBody(selectedNote?.body ?? '');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>Cancel</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryButtonText}>Cancel</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Save note changes"
|
{...buttonA11y('Save note changes')}
|
||||||
style={styles.primaryButton}
|
style={styles.primaryButton}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
await updateNote(noteId, draftTitle, draftBody);
|
await updateNote(noteId, draftTitle, draftBody);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.primaryButtonText}>Save</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.primaryButtonText}>Save</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
) : !isLoading ? (
|
) : !isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.body}>{selectedNote?.body ?? 'No note body is available yet.'}</Text>
|
<Text style={styles.body}>{selectedNote?.body ?? 'No note body is available yet.'}</Text>
|
||||||
<Pressable accessibilityLabel="Edit note" style={styles.primaryButton} onPress={() => setIsEditing(true)}>
|
<Pressable {...buttonA11y('Edit note')} style={styles.primaryButton} onPress={() => setIsEditing(true)}>
|
||||||
<Text style={styles.primaryButtonText}>Edit note</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.primaryButtonText}>Edit note</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
@ -115,7 +118,7 @@ export default function NoteDetailScreen() {
|
|||||||
<Text style={styles.sectionTitle}>Share</Text>
|
<Text style={styles.sectionTitle}>Share</Text>
|
||||||
<View style={styles.actionRow}>
|
<View style={styles.actionRow}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Share note as text"
|
{...buttonA11y('Share note as text')}
|
||||||
style={styles.secondaryButton}
|
style={styles.secondaryButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
void Share.share({
|
void Share.share({
|
||||||
@ -124,10 +127,10 @@ export default function NoteDetailScreen() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>Share as text</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryButtonText}>Share as text</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Share deep link to note"
|
{...buttonA11y('Share deep link to note')}
|
||||||
style={styles.secondaryButton}
|
style={styles.secondaryButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
void Share.share({
|
void Share.share({
|
||||||
@ -136,7 +139,7 @@ export default function NoteDetailScreen() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>Share link</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryButtonText}>Share link</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -145,7 +148,7 @@ export default function NoteDetailScreen() {
|
|||||||
{/* Smart Actions */}
|
{/* Smart Actions */}
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Toggle Smart Actions"
|
{...buttonA11y('Toggle Smart Actions', { expanded: showSmartActions })}
|
||||||
style={styles.actionRow}
|
style={styles.actionRow}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShowSmartActions(!showSmartActions);
|
setShowSmartActions(!showSmartActions);
|
||||||
@ -170,7 +173,7 @@ export default function NoteDetailScreen() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel="Suggest tags"
|
{...buttonA11y('Suggest tags')}
|
||||||
style={styles.secondaryButton}
|
style={styles.secondaryButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!selectedNote) return;
|
if (!selectedNote) return;
|
||||||
@ -179,7 +182,7 @@ export default function NoteDetailScreen() {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>Suggest tags</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryButtonText}>Suggest tags</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
{suggestedTags.length > 0 && (
|
{suggestedTags.length > 0 && (
|
||||||
@ -195,7 +198,7 @@ export default function NoteDetailScreen() {
|
|||||||
{templates.slice(0, 6).map((t) => (
|
{templates.slice(0, 6).map((t) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={t.id}
|
key={t.id}
|
||||||
accessibilityLabel={`Run: ${t.name}`}
|
{...buttonA11y(`Run: ${t.name}`, { disabled: isRunning || !selectedNote })}
|
||||||
style={[styles.secondaryButton, isRunning ? { opacity: 0.5 } : null]}
|
style={[styles.secondaryButton, isRunning ? { opacity: 0.5 } : null]}
|
||||||
disabled={isRunning || !selectedNote}
|
disabled={isRunning || !selectedNote}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@ -207,7 +210,7 @@ export default function NoteDetailScreen() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>{t.name}</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryButtonText}>{t.name}</Text>
|
||||||
<Text style={[styles.body, { fontSize: 12 }]}>{t.description}</Text>
|
<Text style={[styles.body, { fontSize: 12 }]}>{t.description}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
@ -228,8 +231,8 @@ export default function NoteDetailScreen() {
|
|||||||
{lastResult.model} · {lastResult.usage?.totalTokens ?? 0} tokens
|
{lastResult.model} · {lastResult.usage?.totalTokens ?? 0} tokens
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Pressable accessibilityLabel="Dismiss result" style={styles.secondaryButton} onPress={clearResult}>
|
<Pressable {...buttonA11y('Dismiss result')} style={styles.secondaryButton} onPress={clearResult}>
|
||||||
<Text style={styles.secondaryButtonText}>Dismiss</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.secondaryButtonText}>Dismiss</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
|
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
import { usePromptStore } from '../store/prompt-store';
|
import { usePromptStore } from '../store/prompt-store';
|
||||||
|
import { buttonA11y, dynamicType } from '../lib/accessibility';
|
||||||
import { colors } from '../theme';
|
import { colors } from '../theme';
|
||||||
|
|
||||||
export default function PromptResultScreen() {
|
export default function PromptResultScreen() {
|
||||||
@ -12,8 +13,8 @@ export default function PromptResultScreen() {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.empty}>No prompt result available.</Text>
|
<Text style={styles.empty}>No prompt result available.</Text>
|
||||||
<TouchableOpacity style={styles.btn} onPress={() => router.back()}>
|
<TouchableOpacity {...buttonA11y('Go back')} style={styles.btn} onPress={() => router.back()}>
|
||||||
<Text style={styles.btnText}>Go Back</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.btnText}>Go Back</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@ -45,13 +46,14 @@ export default function PromptResultScreen() {
|
|||||||
|
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
{...buttonA11y('Dismiss prompt result')}
|
||||||
style={styles.btn}
|
style={styles.btn}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
clearResult();
|
clearResult();
|
||||||
router.back();
|
router.back();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.btnText}>Dismiss</Text>
|
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.btnText}>Dismiss</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
39
mobile/src/lib/accessibility.ts
Normal file
39
mobile/src/lib/accessibility.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user