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"}');