diff --git a/apps/mobile/src/components/icons.tsx b/apps/mobile/src/components/icons.tsx
index 25eb11fa..f1144b9f 100644
--- a/apps/mobile/src/components/icons.tsx
+++ b/apps/mobile/src/components/icons.tsx
@@ -32,6 +32,7 @@ export const Camera = createIcon("camera");
export const Check = createIcon("check");
export const CheckSquare = createIcon("check-square");
export const ChevronDown = createIcon("chevron-down");
+export const ChevronUp = createIcon("chevron-up");
export const ChevronLeft = createIcon("chevron-left");
export const ChevronRight = createIcon("chevron-right");
export const Code = createIcon("code");
diff --git a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx
index e54f872a..768a07a0 100644
--- a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx
+++ b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx
@@ -11,7 +11,7 @@ import { Image as RNImage, KeyboardAvoidingView, Platform, ScrollView, StyleShee
import { Modal } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { SvgXml } from "react-native-svg";
-import { ActivityIndicator, ChevronDown, ChevronLeft, ChevronRight, Copy, Download, History, MoreHorizontal, Pencil, RotateCcw, Search, Share2, Sparkles, Tag, Trash2, X } from "../components/icons";
+import { ActivityIndicator, ChevronDown, ChevronLeft, ChevronUp, Copy, Download, History, MoreHorizontal, Pencil, RotateCcw, Search, Share2, Sparkles, Tag, Trash2, X } from "../components/icons";
import { Alert, Pressable, Text, TextInput } from "../components/LocalizedText";
import LocalTiptapEditor, { type LocalTiptapEditorRef } from "../components/LocalTiptapEditor";
import { MobileAiAssistantModal } from "../components/MobileAiAssistantModal";
@@ -362,13 +362,6 @@ const DetailActionSheetItem = ({ danger = false, disabled = false, icon, label,
);
-const DetailActionButton = ({ children, disabled = false, label, onPress }: { children: ReactNode; disabled?: boolean; label: string; onPress: () => void }) => (
-
- {children}
- {label}
-
-);
-
const HighlightedMetadataText = ({
activeIndex,
matchOffset,
@@ -733,6 +726,13 @@ export const MemoDetailModal = ({
}
setActiveMatchIndex((current) => getNextMobileNoteSearchIndex(current, direction, searchMatchCount));
};
+ const closeNoteSearch = () => {
+ setSearchOpen(false);
+ setSearchQuery("");
+ setBodySearchMatchCount(0);
+ setActiveMatchIndex(0);
+ safeDomCall(() => viewerRef.current?.search("", -1));
+ };
const closeActionsAndRun = (action: () => void) => {
setActionsOpen(false);
@@ -1183,41 +1183,58 @@ export const MemoDetailModal = ({
{searchOpen ? (
-
-
- {
- setSearchQuery(value);
- setBodySearchMatchCount(0);
- setActiveMatchIndex(0);
- }}
- placeholder="在当前笔记内搜索"
- placeholderTextColor="#94a3b8"
- style={styles.searchInput}
- value={searchQuery}
- />
- {searchMatchLabel}
-
-
- moveSearchMatch(-1)}>
-
-
- moveSearchMatch(1)}>
-
-
- {
- setSearchOpen(false);
- setSearchQuery("");
+
+ {
+ setSearchQuery(value);
setBodySearchMatchCount(0);
setActiveMatchIndex(0);
- safeDomCall(() => viewerRef.current?.search("", -1));
- }}>
-
-
-
+ }}
+ onSubmitEditing={() => moveSearchMatch(1)}
+ placeholder="搜索"
+ placeholderTextColor="#94a3b8"
+ returnKeyType="search"
+ style={styles.noteSearchInput}
+ value={searchQuery}
+ />
+
+ {searchMatchLabel}
+
+ moveSearchMatch(-1)}
+ style={[styles.noteSearchIconButton, searchMatchCount === 0 && styles.buttonDisabled]}
+ >
+
+
+ moveSearchMatch(1)}
+ style={[styles.noteSearchIconButton, searchMatchCount === 0 && styles.buttonDisabled]}
+ >
+
+
+
+
+
) : null}
diff --git a/apps/mobile/src/screens/workspace-styles.ts b/apps/mobile/src/screens/workspace-styles.ts
index 86a5432d..b223bf1d 100644
--- a/apps/mobile/src/screens/workspace-styles.ts
+++ b/apps/mobile/src/screens/workspace-styles.ts
@@ -1949,18 +1949,41 @@ const baseWorkspaceStyles = StyleSheet.create({
marginTop: 20,
},
noteSearchPanel: {
- backgroundColor: "#f8fafc",
+ alignItems: "center",
+ backgroundColor: "#ffffff",
borderColor: "#e2e8f0",
borderRadius: 8,
borderWidth: 1,
- gap: 10,
- marginTop: 14,
- padding: 10,
+ flexDirection: "row",
+ gap: 2,
+ marginTop: 12,
+ minHeight: 40,
+ paddingLeft: 10,
+ paddingRight: 2,
+ },
+ noteSearchInput: {
+ color: "#0f172a",
+ flex: 1,
+ fontSize: 15,
+ minHeight: 36,
+ minWidth: 0,
+ paddingVertical: 0,
+ },
+ noteSearchIconButton: {
+ alignItems: "center",
+ flexShrink: 0,
+ height: 36,
+ justifyContent: "center",
+ width: 36,
},
noteSearchCount: {
color: "#64748b",
+ flexShrink: 0,
fontSize: 12,
- fontWeight: "800",
+ fontVariant: ["tabular-nums"],
+ fontWeight: "700",
+ minWidth: 44,
+ textAlign: "right",
},
noteSearchCountEmpty: {
color: "#b91c1c",
diff --git a/tests/mobile-app-scope.test.ts b/tests/mobile-app-scope.test.ts
index dc466411..1dd9e4cc 100644
--- a/tests/mobile-app-scope.test.ts
+++ b/tests/mobile-app-scope.test.ts
@@ -128,6 +128,20 @@ describe("mobile app scope", () => {
expect(localTiptapEditorSource).toContain("scrollEditorPositionIntoView(editor, match.from");
});
+ test("keeps in-note search on one icon row", () => {
+ expect(memoDetailSource).toContain('accessibilityLabel="上一个搜索结果"');
+ expect(memoDetailSource).toContain('accessibilityLabel="下一个搜索结果"');
+ expect(memoDetailSource).toContain('accessibilityLabel="关闭搜索"');
+ expect(memoDetailSource).not.toContain("label=\"上一个搜索结果\"");
+ expect(memoDetailSource).not.toContain("label=\"下一个搜索结果\"");
+ expect(memoDetailSource).not.toContain("label=\"关闭搜索\"");
+ const stylesSource = readFileSync(
+ new URL("../apps/mobile/src/screens/workspace-styles.ts", import.meta.url),
+ "utf8",
+ );
+ expect(stylesSource).toMatch(/noteSearchPanel: \{[\s\S]*?flexDirection: "row"/);
+ });
+
test("keeps the Android editor caret visible while the keyboard viewport changes", () => {
expect(workspaceEditorsSource).toContain("KeyboardAvoidingView");
expect(workspaceEditorsSource).toContain('enabled={Platform.OS === "android"}');