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,