Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 30 additions & 23 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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. */}
<PressableCard
className="mb-5 flex-row items-center p-4"
onPress={() => router.push("/ai")}
accessibilityRole="button"
accessibilityLabel={t("home.assistantTitle")}
>
<View className="rounded-xl bg-primary/10 p-3">
<Sparkles size={24} color={accent} />
</View>
<View className="ms-4 flex-1">
<Text className="text-base font-semibold text-card-foreground">
{t("home.assistantTitle")}
</Text>
<Text className="mt-0.5 text-sm text-muted-foreground">
{t("home.assistantSubtitle")}
</Text>
</View>
<ChevronRight size={20} color="#94a3b8" />
</PressableCard>
<Animated.View entering={FadeInDown.duration(380)}>
<PressableCard
className="mb-5 flex-row items-center p-4"
onPress={() => router.push("/ai")}
accessibilityRole="button"
accessibilityLabel={t("home.assistantTitle")}
>
<View className="rounded-xl bg-primary/10 p-3">
<Sparkles size={24} color={accent} />
</View>
<View className="ms-4 flex-1">
<Text className="text-base font-semibold text-card-foreground">
{t("home.assistantTitle")}
</Text>
<Text className="mt-0.5 text-sm text-muted-foreground">
{t("home.assistantSubtitle")}
</Text>
</View>
<ChevronRight size={20} color="#94a3b8" />
</PressableCard>
</Animated.View>

{/* Recently viewed records — quick re-entry to what you were working on. */}
{recents.length > 0 && (
<View className="mb-5">
<Animated.View entering={FadeInDown.delay(70).duration(380)} className="mb-5">
<View className="mb-2 flex-row items-center justify-between">
<Text className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{t("home.recentTitle")}
Expand Down Expand Up @@ -223,7 +226,7 @@ export default function HomeScreen() {
</PressableCard>
))}
</View>
</View>
</Animated.View>
)}

{loading ? (
Expand Down Expand Up @@ -252,9 +255,12 @@ export default function HomeScreen() {
<Text className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{t("home.dashboardsTitle")}
</Text>
{dashboards.map((d) => (
<PressableCard
{dashboards.map((d, i) => (
<Animated.View
key={`${d.appId}:${d.name}`}
entering={FadeInDown.delay(140 + i * 60).duration(380)}
>
<PressableCard
className="flex-row items-center p-4"
onPress={() =>
router.push(`/(app)/${d.appId}/dashboard/${d.name}`)
Expand Down Expand Up @@ -283,6 +289,7 @@ export default function HomeScreen() {
</View>
<ChevronRight size={20} color="#94a3b8" />
</PressableCard>
</Animated.View>
))}
</View>
)}
Expand Down
6 changes: 4 additions & 2 deletions components/renderers/DetailViewRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -709,8 +710,9 @@ export function DetailViewRenderer({
// Conditional section visibility (spec `FormSection.visibleOn`).
if (!isSectionVisible(section.visibleOn, record)) return null;
return (
<View
<Animated.View
key={section.label ?? `section-${idx}`}
entering={FadeInDown.delay(idx * 60).duration(360)}
className="mb-4 rounded-xl border border-border bg-card overflow-hidden"
>
{section.label && (
Expand Down Expand Up @@ -776,7 +778,7 @@ export function DetailViewRenderer({
);
})}
</View>
</View>
</Animated.View>
);
})}

Expand Down
62 changes: 62 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {};
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<string, unknown>, 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,
Expand Down
Loading