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
23 changes: 15 additions & 8 deletions mobile/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// browser behind it keeps its tabs of WebViews, address bar, and the shared
// suggestion engine from core/.
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ActivityIndicator, BackHandler, Keyboard, PanResponder, Platform, Pressable, Text, TextInput, View, useWindowDimensions } from "react-native";
import { AccessibilityInfo, ActivityIndicator, BackHandler, Keyboard, PanResponder, Platform, Pressable, Text, TextInput, View, useWindowDimensions } from "react-native";
import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";

Expand Down Expand Up @@ -217,6 +217,7 @@ function Browser({ services }) {
// recorded as a capture card. No screen change; a note with Undo shows.
const showToast = (t) => {
clearTimeout(toastTimer.current);
AccessibilityInfo.announceForAccessibility?.(t.text); // VoiceOver reads the note; TalkBack uses the live region
setToast(t);
toastTimer.current = setTimeout(() => setToast(null), 6000);
};
Expand Down Expand Up @@ -397,8 +398,8 @@ function Browser({ services }) {
{/* Address bar, with suggestions from history and Wikipedia while typing */}
<View style={{ zIndex: 20, elevation: 20 }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 6, padding: 8, borderBottomWidth: 1, borderColor: colors.line }} {...addressPan.panHandlers}>
<Button label="‹" small onPress={() => refs.current.get(activeId)?.goBack()} style={!active?.canGoBack && { opacity: 0.4 }} />
<Button label="›" small onPress={() => refs.current.get(activeId)?.goForward()} style={!active?.canGoForward && { opacity: 0.4 }} />
<Button label="‹" a11yLabel="Back" small onPress={() => refs.current.get(activeId)?.goBack()} style={!active?.canGoBack && { opacity: 0.4 }} />
<Button label="›" a11yLabel="Forward" small onPress={() => refs.current.get(activeId)?.goForward()} style={!active?.canGoForward && { opacity: 0.4 }} />
<TextInput
style={[ui.input, { flex: 1, paddingVertical: 6 }]}
value={editing ? address : active?.url || ""}
Expand Down Expand Up @@ -432,7 +433,7 @@ function Browser({ services }) {
}}
/>
{/* Tap: tab list. Press and hold: new tab. */}
<Button label={String(tabs.length)} small onPress={() => setSheet("tabs")} onLongPress={() => addTab(HOME_URL)} />
<Button label={String(tabs.length)} a11yLabel={`${tabs.length} ${tabs.length === 1 ? "tab" : "tabs"}, open the tab list`} a11yHint="Press and hold for a new tab" small onPress={() => setSheet("tabs")} onLongPress={() => addTab(HOME_URL)} />
</View>
{editing ? (
<AddressSuggestions
Expand Down Expand Up @@ -484,7 +485,13 @@ function Browser({ services }) {
/>
{showMosaic && !isActive ? (
// Tap an inactive tile to make it the active tab; then it takes touches.
<Pressable style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }} onPress={() => switchTo(t.id)} />
<Pressable
style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }}
onPress={() => switchTo(t.id)}
accessibilityRole="button"
accessibilityLabel={`Mosaic tile ${tile + 1}: ${t.title || t.url || "new tab"}`}
accessibilityHint="Makes this the active tab"
/>
) : null}
</View>
);
Expand All @@ -494,10 +501,10 @@ function Browser({ services }) {
{/* Bottom bar */}
<View style={{ flexDirection: "row", gap: 6, padding: 8, borderTopWidth: 1, borderColor: colors.line, alignItems: "center" }}>
<Button label={`Suggest${state.results.length ? ` (${state.results.length})` : ""}`} active={state.busy} onPress={() => setSheet("suggest")} />
<Button label="Capture" onPress={capture} />
<Button label="Capture" a11yHint="Saves this page, the selected text and a thumbnail into the trail" onPress={capture} />
<Button label="Mosaic" active={mosaic} onPress={() => setMosaic(!mosaic)} />
<Button label="Trail" onPress={() => setHome(true)} />
<Button label="⚙︎" onPress={() => setSheet("settings")} />
<Button label="⚙︎" a11yLabel="Settings" onPress={() => setSheet("settings")} />
<Text style={[ui.help, { flex: 1 }]} numberOfLines={1}>
{state.busy ? "Searching…" : ""}
</Text>
Expand Down Expand Up @@ -539,7 +546,7 @@ function Browser({ services }) {
<SettingsSheet visible={sheet === "settings"} onClose={() => setSheet(null)} services={services} />

{toast && !home ? (
<View style={{ position: "absolute", left: 12, right: 12, bottom: 70, zIndex: 40, elevation: 40, flexDirection: "row", alignItems: "center", gap: 8, backgroundColor: colors.bg3, borderWidth: 1, borderColor: colors.line, borderRadius: 10, padding: 10 }}>
<View accessibilityLiveRegion="polite" style={{ position: "absolute", left: 12, right: 12, bottom: 70, zIndex: 40, elevation: 40, flexDirection: "row", alignItems: "center", gap: 8, backgroundColor: colors.bg3, borderWidth: 1, borderColor: colors.line, borderRadius: 10, padding: 10 }}>
<Text style={{ color: colors.text, flex: 1 }} numberOfLines={2}>
{toast.text}
</Text>
Expand Down
1 change: 1 addition & 0 deletions mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Phase 5 groundwork. A React Native (Expo SDK 58, React Native 0.88) app that reu
- **Open a `.trail.md`** (CM3): Open file → the file opens read only, on Replay: the sender's note, Start here, the steps with captions (Back, Next, tap a step); the Canvas tab dims cards not reached yet and follows the step. Save as new trail makes an editable copy, "<name> · <sender>", and goes to its timeline. Open on desktop hands the file to the share sheet (AirDrop it to your Mac)
- **Share** (CM4, CM5): a note, Start here, the timeline on or off, your name; file name and size; "Share file…" opens the phone's share sheet with the `.trail.md`. Sign-in and payment pages are never put in the file
- **Capture** (M18, bottom bar): one tap saves the page into the trail being recorded: address, title, DOI or PMID if the page has one, the time with the time zone, the selected text (or the last text you selected on the page) and a thumbnail. It becomes a capture card linked "captured from" the page's card, as on desktop. A note says where it went, with Undo. Sign-in and payment pages are never captured. The card's preview shows the thumbnail and Open DOI / PubMed. Shared files carry the quote and the "Captured from … · DOI …" line, not the thumbnail
- **Screen readers** (M25): every button, tab, card, Mosaic tile, keyword and result has a spoken label and role; sheet titles and timeline sessions are headings; Timeline / Canvas / Replay are tabs. Gestures have screen-reader actions: next and previous trail on the switcher, hide on a keyword, close on a tab row. The capture note is announced. Every grid has a list: the Timeline for the canvas, the tab list for Mosaic
- Pages you read on the phone go into the current trail's timeline as phone visits (timeline only, as decided in doc 25); cards are placed on desktop. Old research sessions are copied into trails on the first start
- Tabs, each a WebView kept loaded; address bar with back and forward; tab switcher
- Mosaic on every screen (Mosaic button in the bottom bar): all tabs in a grid on tablets and phones in landscape; up to 4 tiles on a phone in portrait (the active tab plus the 3 most recent others)
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/components/AddressSuggestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default function AddressSuggestions({ query, history, bookmarks, getText,
{/* "handled": tapping a row does not first close the keyboard and the list. */}
<ScrollView keyboardShouldPersistTaps="handled">
{rows.map((r) => (
<Pressable key={r.key} onPress={() => onPick(r.url)} style={{ paddingHorizontal: 12, paddingVertical: 9, borderTopWidth: 1, borderColor: colors.line }}>
<Pressable key={r.key} onPress={() => onPick(r.url)} accessibilityRole="button" accessibilityLabel={`${r.history ? "From history: " : ""}${r.label}`} style={{ paddingHorizontal: 12, paddingVertical: 9, borderTopWidth: 1, borderColor: colors.line }}>
<Text style={{ color: colors.text, fontSize: 14 }} numberOfLines={1}>
{r.history ? "🕘 " : ""}
{r.label}
Expand Down
7 changes: 6 additions & 1 deletion mobile/src/components/Button.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Pressable, Text } from "react-native";
import { colors, ui } from "../theme";

export default function Button({ label, onPress, onLongPress, active, style, small }) {
// a11yLabel: what a screen reader says when the label is a symbol (‹, ⚙︎, +).
export default function Button({ label, onPress, onLongPress, active, style, small, a11yLabel, a11yHint }) {
return (
<Pressable
onPress={onPress}
onLongPress={onLongPress}
accessibilityRole="button"
accessibilityLabel={a11yLabel || String(label)}
accessibilityHint={a11yHint}
accessibilityState={active !== undefined ? { selected: Boolean(active) } : undefined}
style={({ pressed }) => [ui.button, small && { paddingVertical: 4, paddingHorizontal: 8 }, active && { borderColor: colors.accent }, pressed && { opacity: 0.6 }, style]}
>
<Text style={[ui.buttonText, active && { color: colors.accent }, small && { fontSize: 12 }]}>{label}</Text>
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/components/Sheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function Sheet({ visible, title, kicker, onClose, children, scrol
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", padding: 14, paddingBottom: 6 }}>
<View>
{kicker ? <Text style={[ui.kicker, { marginTop: 0 }]}>{kicker}</Text> : null}
<Text style={ui.title}>{title}</Text>
<Text style={ui.title} accessibilityRole="header">{title}</Text>
</View>
<Button label="Close" onPress={onClose} small />
</View>
Expand Down
16 changes: 13 additions & 3 deletions mobile/src/components/SuggestionsSheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,22 @@ export default function SuggestionsSheet({ visible, onClose, state, controller,
<Text style={ui.help}>Tap a keyword to pin it. Press and hold to hide it.</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", marginTop: 8 }}>
{pins.map((k) => (
<Pressable key={"p" + k} style={[ui.chip, { borderColor: colors.accent }]} onPress={() => controller.togglePin(k)}>
<Pressable key={"p" + k} style={[ui.chip, { borderColor: colors.accent }]} onPress={() => controller.togglePin(k)} accessibilityRole="button" accessibilityLabel={`${k}, pinned`} accessibilityHint="Unpins this keyword">
<Text style={[ui.chipText, { color: colors.accent }]}>📌 {k}</Text>
</Pressable>
))}
{auto.map((k) => (
<Pressable key={"a" + k} style={ui.chip} onPress={() => controller.togglePin(k)} onLongPress={() => controller.hide(k)}>
<Pressable
key={"a" + k}
style={ui.chip}
onPress={() => controller.togglePin(k)}
onLongPress={() => controller.hide(k)}
accessibilityRole="button"
accessibilityLabel={k}
accessibilityHint="Pins this keyword. Actions: hide it"
accessibilityActions={[{ name: "hide", label: "Hide keyword" }]}
onAccessibilityAction={(e) => e.nativeEvent.actionName === "hide" && controller.hide(k)}
>
<Text style={ui.chipText}>{k}</Text>
</Pressable>
))}
Expand All @@ -52,7 +62,7 @@ export default function SuggestionsSheet({ visible, onClose, state, controller,
{r.source}
{r.query ? <Text style={{ color: colors.mute, textTransform: "none" }}> · {r.query}</Text> : null}
</Text>
<Pressable onPress={() => controller.dismiss(r.url)} hitSlop={10}>
<Pressable onPress={() => controller.dismiss(r.url)} hitSlop={10} accessibilityRole="button" accessibilityLabel={`Dismiss ${r.title}`}>
<Text style={{ color: colors.mute }}>×</Text>
</Pressable>
</View>
Expand Down
8 changes: 6 additions & 2 deletions mobile/src/components/TabsSheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export default function TabsSheet({ visible, onClose, tabs, activeId, onActivate
<SwipeRow key={t.id} onSwipe={() => onCloseTab(t.id)}>
<Pressable
onPress={() => onActivate(t.id)}
accessibilityRole="button"
accessibilityLabel={`${t.title || "New tab"}${t.id === activeId ? ", current tab" : ""}`}
accessibilityActions={[{ name: "close", label: "Close tab" }]}
onAccessibilityAction={(e) => e.nativeEvent.actionName === "close" && onCloseTab(t.id)}
style={{ flexDirection: "row", alignItems: "center", borderTopWidth: 1, borderColor: colors.line, paddingVertical: 10 }}
>
<View style={{ flex: 1 }}>
Expand All @@ -26,7 +30,7 @@ export default function TabsSheet({ visible, onClose, tabs, activeId, onActivate
</Text>
<Text style={ui.help} numberOfLines={1}>{t.pausedByLimit ? "⏸ Paused (4 tabs already playing) · " : ""}{t.sysPauses ? `Paused by the system ${t.sysPauses}× and restarted · ` : ""}{t.url}</Text>
</View>
<Pressable onPress={() => onCloseTab(t.id)} hitSlop={10} style={{ paddingHorizontal: 8 }}>
<Pressable onPress={() => onCloseTab(t.id)} hitSlop={10} style={{ paddingHorizontal: 8 }} accessibilityRole="button" accessibilityLabel={`Close ${t.title || "tab"}`}>
<Text style={{ color: colors.mute, fontSize: 18 }}>×</Text>
</Pressable>
</Pressable>
Expand All @@ -38,7 +42,7 @@ export default function TabsSheet({ visible, onClose, tabs, activeId, onActivate
{bookmarks.search("", 100).length === 0 ? <Text style={ui.help}>No bookmarks yet. Tap ☆ next to the address bar to add the page you are on.</Text> : null}
{bookmarks.search("", 100).map((b) => (
<SwipeRow key={"b" + b.url} label="Remove" onSwipe={() => onRemoveBookmark(b.url)}>
<Pressable onPress={() => onOpenBookmark(b.url)} style={{ borderTopWidth: 1, borderColor: colors.line, paddingVertical: 10 }}>
<Pressable onPress={() => onOpenBookmark(b.url)} accessibilityRole="link" style={{ borderTopWidth: 1, borderColor: colors.line, paddingVertical: 10 }}>
<Text style={{ color: colors.text, fontWeight: "600" }} numberOfLines={1}>★ {b.title || b.url}</Text>
<Text style={ui.help} numberOfLines={1}>{b.url}</Text>
</Pressable>
Expand Down
11 changes: 8 additions & 3 deletions mobile/src/components/TrailCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ export default function TrailCanvas({ snap, onCard, shown = null, focus = null }
<Pressable
key={c.id}
onPress={() => onCard?.(c)}
accessibilityRole="button"
accessibilityLabel={`${start ? "Start here. " : ""}${f.kicker}: ${f.title}${f.text ? `. ${f.text}` : ""}`}
style={{ position: "absolute", left: c.x, top: c.y, width: c.w, height: c.h, borderRadius: 10, borderWidth: focus === c.id || start ? 3 : 1, borderColor: focus === c.id ? colors.accent : start ? colors.ok : colors.line, backgroundColor: colors.bg3, padding: 14, opacity: dim(c.id) ? 0.2 : 1, overflow: "hidden" }}
>
<Text style={{ color: colors.accent, fontSize: 13, letterSpacing: 1, textTransform: "uppercase" }} numberOfLines={1}>
Expand Down Expand Up @@ -152,9 +154,9 @@ export default function TrailCanvas({ snap, onCard, shown = null, focus = null }
{view && size && !empty ? <Minimap snap={snap} bounds={bounds} view={view} size={size} onJump={(wx, wy) => setView(M.centreOn(view, wx, wy, size.w, size.h))} /> : null}
{view && size && !empty ? (
<View style={{ position: "absolute", left: 10, bottom: 10, flexDirection: "row", gap: 6 }}>
<Button label="Fit" small onPress={fit} />
<Button label="−" small onPress={() => setView(M.zoomAt(view, 1 / 1.4, size.w / 2, size.h / 2))} />
<Button label="+" small onPress={() => setView(M.zoomAt(view, 1.4, size.w / 2, size.h / 2))} />
<Button label="Fit" a11yLabel="Fit the whole trail" small onPress={fit} />
<Button label="−" a11yLabel="Zoom out" small onPress={() => setView(M.zoomAt(view, 1 / 1.4, size.w / 2, size.h / 2))} />
<Button label="+" a11yLabel="Zoom in" small onPress={() => setView(M.zoomAt(view, 1.4, size.w / 2, size.h / 2))} />
</View>
) : null}
</View>
Expand All @@ -172,6 +174,9 @@ function Minimap({ snap, bounds, view, size, onJump }) {
const vp = at(vx, vy);
return (
<Pressable
accessibilityRole="button"
accessibilityLabel="Minimap"
accessibilityHint="Moves the canvas to where you tap. The Timeline tab lists every card."
onPress={(e) => onJump(bounds.x + (e.nativeEvent.locationX - ox) / k, bounds.y + (e.nativeEvent.locationY - oy) / k)}
style={{ position: "absolute", right: 10, bottom: 10, width: MINI_W, height: MINI_H, backgroundColor: "rgba(26,26,26,0.92)", borderRadius: 8, borderWidth: 1, borderColor: colors.line, overflow: "hidden" }}
>
Expand Down
Loading