diff --git a/mobile/App.js b/mobile/App.js
index cf70493..4dd3f21 100644
--- a/mobile/App.js
+++ b/mobile/App.js
@@ -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";
@@ -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);
};
@@ -397,8 +398,8 @@ function Browser({ services }) {
{/* Address bar, with suggestions from history and Wikipedia while typing */}
-
{editing ? (
{showMosaic && !isActive ? (
// Tap an inactive tile to make it the active tab; then it takes touches.
- switchTo(t.id)} />
+ switchTo(t.id)}
+ accessibilityRole="button"
+ accessibilityLabel={`Mosaic tile ${tile + 1}: ${t.title || t.url || "new tab"}`}
+ accessibilityHint="Makes this the active tab"
+ />
) : null}
);
@@ -494,10 +501,10 @@ function Browser({ services }) {
{/* Bottom bar */}
setSheet("suggest")} />
-
+
setMosaic(!mosaic)} />
setHome(true)} />
- setSheet("settings")} />
+ setSheet("settings")} />
{state.busy ? "Searching…" : ""}
@@ -539,7 +546,7 @@ function Browser({ services }) {
setSheet(null)} services={services} />
{toast && !home ? (
-
+
{toast.text}
diff --git a/mobile/README.md b/mobile/README.md
index 1826ccd..02ab091 100644
--- a/mobile/README.md
+++ b/mobile/README.md
@@ -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, " · ", 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)
diff --git a/mobile/src/components/AddressSuggestions.js b/mobile/src/components/AddressSuggestions.js
index 25f8808..5547fee 100644
--- a/mobile/src/components/AddressSuggestions.js
+++ b/mobile/src/components/AddressSuggestions.js
@@ -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. */}
{rows.map((r) => (
- onPick(r.url)} style={{ paddingHorizontal: 12, paddingVertical: 9, borderTopWidth: 1, borderColor: colors.line }}>
+ onPick(r.url)} accessibilityRole="button" accessibilityLabel={`${r.history ? "From history: " : ""}${r.label}`} style={{ paddingHorizontal: 12, paddingVertical: 9, borderTopWidth: 1, borderColor: colors.line }}>
{r.history ? "🕘 " : ""}
{r.label}
diff --git a/mobile/src/components/Button.js b/mobile/src/components/Button.js
index 7703af0..e5e7ae7 100644
--- a/mobile/src/components/Button.js
+++ b/mobile/src/components/Button.js
@@ -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 (
[ui.button, small && { paddingVertical: 4, paddingHorizontal: 8 }, active && { borderColor: colors.accent }, pressed && { opacity: 0.6 }, style]}
>
{label}
diff --git a/mobile/src/components/Sheet.js b/mobile/src/components/Sheet.js
index a6d76cf..39d5c06 100644
--- a/mobile/src/components/Sheet.js
+++ b/mobile/src/components/Sheet.js
@@ -12,7 +12,7 @@ export default function Sheet({ visible, title, kicker, onClose, children, scrol
{kicker ? {kicker} : null}
- {title}
+ {title}
diff --git a/mobile/src/components/SuggestionsSheet.js b/mobile/src/components/SuggestionsSheet.js
index 22208e0..2d62301 100644
--- a/mobile/src/components/SuggestionsSheet.js
+++ b/mobile/src/components/SuggestionsSheet.js
@@ -21,12 +21,22 @@ export default function SuggestionsSheet({ visible, onClose, state, controller,
Tap a keyword to pin it. Press and hold to hide it.
{pins.map((k) => (
- controller.togglePin(k)}>
+ controller.togglePin(k)} accessibilityRole="button" accessibilityLabel={`${k}, pinned`} accessibilityHint="Unpins this keyword">
📌 {k}
))}
{auto.map((k) => (
- controller.togglePin(k)} onLongPress={() => controller.hide(k)}>
+ 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)}
+ >
{k}
))}
@@ -52,7 +62,7 @@ export default function SuggestionsSheet({ visible, onClose, state, controller,
{r.source}
{r.query ? · {r.query} : null}
- controller.dismiss(r.url)} hitSlop={10}>
+ controller.dismiss(r.url)} hitSlop={10} accessibilityRole="button" accessibilityLabel={`Dismiss ${r.title}`}>
×
diff --git a/mobile/src/components/TabsSheet.js b/mobile/src/components/TabsSheet.js
index bb8688e..1c88338 100644
--- a/mobile/src/components/TabsSheet.js
+++ b/mobile/src/components/TabsSheet.js
@@ -18,6 +18,10 @@ export default function TabsSheet({ visible, onClose, tabs, activeId, onActivate
onCloseTab(t.id)}>
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 }}
>
@@ -26,7 +30,7 @@ export default function TabsSheet({ visible, onClose, tabs, activeId, onActivate
{t.pausedByLimit ? "⏸ Paused (4 tabs already playing) · " : ""}{t.sysPauses ? `Paused by the system ${t.sysPauses}× and restarted · ` : ""}{t.url}
- onCloseTab(t.id)} hitSlop={10} style={{ paddingHorizontal: 8 }}>
+ onCloseTab(t.id)} hitSlop={10} style={{ paddingHorizontal: 8 }} accessibilityRole="button" accessibilityLabel={`Close ${t.title || "tab"}`}>
×
@@ -38,7 +42,7 @@ export default function TabsSheet({ visible, onClose, tabs, activeId, onActivate
{bookmarks.search("", 100).length === 0 ? No bookmarks yet. Tap ☆ next to the address bar to add the page you are on. : null}
{bookmarks.search("", 100).map((b) => (
onRemoveBookmark(b.url)}>
- onOpenBookmark(b.url)} style={{ borderTopWidth: 1, borderColor: colors.line, paddingVertical: 10 }}>
+ onOpenBookmark(b.url)} accessibilityRole="link" style={{ borderTopWidth: 1, borderColor: colors.line, paddingVertical: 10 }}>
★ {b.title || b.url}
{b.url}
diff --git a/mobile/src/components/TrailCanvas.js b/mobile/src/components/TrailCanvas.js
index 1602abe..8a9b363 100644
--- a/mobile/src/components/TrailCanvas.js
+++ b/mobile/src/components/TrailCanvas.js
@@ -124,6 +124,8 @@ export default function TrailCanvas({ snap, onCard, shown = null, focus = null }
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" }}
>
@@ -152,9 +154,9 @@ export default function TrailCanvas({ snap, onCard, shown = null, focus = null }
{view && size && !empty ? setView(M.centreOn(view, wx, wy, size.w, size.h))} /> : null}
{view && size && !empty ? (
-
- setView(M.zoomAt(view, 1 / 1.4, size.w / 2, size.h / 2))} />
- setView(M.zoomAt(view, 1.4, size.w / 2, size.h / 2))} />
+
+ setView(M.zoomAt(view, 1 / 1.4, size.w / 2, size.h / 2))} />
+ setView(M.zoomAt(view, 1.4, size.w / 2, size.h / 2))} />
) : null}
@@ -172,6 +174,9 @@ function Minimap({ snap, bounds, view, size, onJump }) {
const vp = at(vx, vy);
return (
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" }}
>
diff --git a/mobile/src/components/TrailHome.js b/mobile/src/components/TrailHome.js
index 48b029c..a6b541a 100644
--- a/mobile/src/components/TrailHome.js
+++ b/mobile/src/components/TrailHome.js
@@ -121,7 +121,21 @@ export default function TrailHome({ services, controller, onBrowse, onOpenUrl, o
{/* Trail switcher: initials, name, last visit, dots; Share next to it. */}
- setMenu(true)} style={{ flexDirection: "row", alignItems: "center", gap: 10, flex: 1 }} accessibilityRole="button" accessibilityLabel="Trails">
+ setMenu(true)}
+ style={{ flexDirection: "row", alignItems: "center", gap: 10, flex: 1 }}
+ accessibilityRole="button"
+ accessibilityLabel={`Trail: ${snap?.trail.name || "Trail"}${readonly ? `, from ${snap.trail.from || "someone"}, read only` : ""}`}
+ accessibilityHint="Opens the list of trails"
+ accessibilityActions={[
+ { name: "next", label: "Next trail" },
+ { name: "previous", label: "Previous trail" },
+ ]}
+ onAccessibilityAction={(e) => {
+ const next = list[index + (e.nativeEvent.actionName === "next" ? 1 : -1)];
+ if (next) show(next.id);
+ }}
+ >
{snap?.trail.mono || "?"}
@@ -135,12 +149,12 @@ export default function TrailHome({ services, controller, onBrowse, onOpenUrl, o
- (readonly ? openOnDesktop() : setSharing(true))} />
+ (readonly ? openOnDesktop() : setSharing(true))} />
{list.length > 1 ? (
{list.slice(0, MAX_DOTS).map((t) => (
- show(t.id)} hitSlop={6}>
+ show(t.id)} hitSlop={6} accessibilityRole="button" accessibilityLabel={`Trail: ${t.name}`} accessibilityState={{ selected: t.id === id }}>
))}
@@ -170,7 +184,7 @@ export default function TrailHome({ services, controller, onBrowse, onOpenUrl, o
{tabs.map(([k, label]) => (
- setTab(k)} style={{ flex: 1, paddingVertical: 7, alignItems: "center", backgroundColor: tab === k ? colors.bg3 : "transparent" }}>
+ setTab(k)} accessibilityRole="tab" accessibilityState={{ selected: tab === k }} accessibilityLabel={label} style={{ flex: 1, paddingVertical: 7, alignItems: "center", backgroundColor: tab === k ? colors.bg3 : "transparent" }}>
{label}
))}
diff --git a/mobile/src/components/TrailTimeline.js b/mobile/src/components/TrailTimeline.js
index 8aa7231..440dace 100644
--- a/mobile/src/components/TrailTimeline.js
+++ b/mobile/src/components/TrailTimeline.js
@@ -21,11 +21,14 @@ export default function TrailTimeline({ snap, header, onItem }) {
}
renderItem={({ item }) => {
- if (item.head) return {item.head};
+ if (item.head) return {item.head};
const face = item.card ? M.cardFace(item.card) : null;
return (
onItem(item)}
+ accessibilityRole="button"
+ accessibilityLabel={[item.time, face ? `${face.kicker}: ${face.title}` : item.title, face?.text || item.sub].filter(Boolean).join(". ")}
+ accessibilityHint={item.card ? "Opens the card's preview" : "Opens the page"}
style={({ pressed }) => ({ flexDirection: "row", gap: 10, paddingVertical: 10, borderTopWidth: 1, borderColor: colors.line, opacity: pressed ? 0.6 : 1 })}
>
{item.time}