diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 7a23f81..7e46697 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,5 +1,6 @@ import { View, Text, ScrollView, Pressable, RefreshControl } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import Animated, { FadeInDown } from "react-native-reanimated"; import { useColorScheme } from "nativewind"; import { webContentMaxWidth } from "~/lib/responsive"; import { @@ -152,29 +153,31 @@ export default function HomeScreen() { {/* AI Assistant quick entry — surfaces the assistant on the home screen instead of burying it two levels deep under More. */} - router.push("/ai")} - accessibilityRole="button" - accessibilityLabel={t("home.assistantTitle")} - > - - - - - - {t("home.assistantTitle")} - - - {t("home.assistantSubtitle")} - - - - + + router.push("/ai")} + accessibilityRole="button" + accessibilityLabel={t("home.assistantTitle")} + > + + + + + + {t("home.assistantTitle")} + + + {t("home.assistantSubtitle")} + + + + + {/* Recently viewed records — quick re-entry to what you were working on. */} {recents.length > 0 && ( - + {t("home.recentTitle")} @@ -223,7 +226,7 @@ export default function HomeScreen() { ))} - + )} {loading ? ( @@ -252,9 +255,12 @@ export default function HomeScreen() { {t("home.dashboardsTitle")} - {dashboards.map((d) => ( - ( + + router.push(`/(app)/${d.appId}/dashboard/${d.name}`) @@ -283,6 +289,7 @@ export default function HomeScreen() { + ))} )} diff --git a/components/renderers/DetailViewRenderer.tsx b/components/renderers/DetailViewRenderer.tsx index d13c5bc..3bf0736 100644 --- a/components/renderers/DetailViewRenderer.tsx +++ b/components/renderers/DetailViewRenderer.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useState } from "react"; import { View, Text, ScrollView, Pressable, ActivityIndicator } from "react-native"; import { useTranslation } from "react-i18next"; import { useColorScheme } from "nativewind"; +import Animated, { FadeInDown } from "react-native-reanimated"; import { webContentMaxWidth } from "~/lib/responsive"; import { Edit, @@ -709,8 +710,9 @@ export function DetailViewRenderer({ // Conditional section visibility (spec `FormSection.visibleOn`). if (!isSectionVisible(section.visibleOn, record)) return null; return ( - {section.label && ( @@ -776,7 +778,7 @@ export function DetailViewRenderer({ ); })} - + ); })} diff --git a/jest.setup.ts b/jest.setup.ts index 039558f..1415fc2 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -197,6 +197,68 @@ jest.mock("@sentry/react-native", () => ({ ), })); +/* ---- react-native-reanimated ---- */ +// Reanimated v4 pulls in react-native-worklets, whose native half throws +// "Worklets isn't initialized" when imported under Node — and the package's +// own bundled mock re-imports the real entrypoint, so it throws too. This +// hand-rolled mock renders Animated.* as plain host components and turns the +// layout-animation builders (FadeInDown.delay().duration()...) into chainable +// no-ops, so screens that use entrance animations mount in tests. +jest.mock("react-native-reanimated", () => { + const React = require("react"); + const RN = require("react-native"); + // A layout-animation builder: every method returns the same chainable stub, + // and it's usable both as `FadeInDown` and `FadeInDown.duration(380)`. + const makeBuilder = () => { + const builder: Record = {}; + const chain = () => builder; + for (const m of ["delay", "duration", "springify", "damping", "stiffness", + "mass", "easing", "withInitialValues", "build", "randomDelay", "reduceMotion"]) { + builder[m] = chain; + } + return builder; + }; + const animations = new Proxy( + {}, + { get: () => new Proxy(makeBuilder(), { get: (t, p) => (t as any)[p] ?? (() => t) }) }, + ); + const createAnimatedComponent = (Component: unknown) => Component; + const Animated = { + View: RN.View, + Text: RN.Text, + ScrollView: RN.ScrollView, + Image: RN.Image, + FlatList: RN.FlatList, + createAnimatedComponent, + }; + return new Proxy( + { + __esModule: true, + default: Animated, + createAnimatedComponent, + useSharedValue: (v: unknown) => ({ value: v }), + useAnimatedStyle: () => ({}), + useDerivedValue: (fn: () => unknown) => ({ value: fn() }), + withTiming: (v: unknown) => v, + withSpring: (v: unknown) => v, + withDelay: (_d: unknown, v: unknown) => v, + withRepeat: (v: unknown) => v, + withSequence: (v: unknown) => v, + cancelAnimation: () => {}, + runOnJS: (fn: (...a: unknown[]) => unknown) => fn, + runOnUI: (fn: (...a: unknown[]) => unknown) => fn, + Easing: new Proxy({}, { get: () => () => 0 }), + }, + { + get: (target: Record, prop: string) => { + if (prop in target) return target[prop]; + // FadeInDown, FadeIn, SlideInRight, Layout, etc. → chainable builder. + return (animations as any)[prop]; + }, + }, + ); +}); + /* ---- react-native AppState ---- */ const mockAppState = { currentState: "active" as string,