Skip to content

Commit f948750

Browse files
Copilothotlong
andcommitted
feat: Phase 9 & 10 - automation hook, package management, analytics explain, report/page/widget renderers, theme bridge
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent f8e2253 commit f948750

15 files changed

Lines changed: 1862 additions & 2 deletions

app/(app)/packages.tsx

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import React from "react";
2+
import {
3+
View,
4+
Text,
5+
ScrollView,
6+
TouchableOpacity,
7+
ActivityIndicator,
8+
Alert,
9+
} from "react-native";
10+
import { Stack } from "expo-router";
11+
import { Package, ToggleLeft, ToggleRight, Trash2 } from "lucide-react-native";
12+
import { usePackageManagement } from "~/hooks/usePackageManagement";
13+
import { Card, CardHeader, CardTitle, CardContent } from "~/components/ui/Card";
14+
15+
/**
16+
* Package management screen – list, enable, disable, uninstall packages.
17+
*
18+
* Route: app/(app)/packages.tsx
19+
*/
20+
export default function PackagesScreen() {
21+
const { packages, isLoading, error, refetch, enable, disable, uninstall } =
22+
usePackageManagement();
23+
24+
const handleToggle = async (id: string, enabled: boolean) => {
25+
try {
26+
if (enabled) {
27+
await disable(id);
28+
} else {
29+
await enable(id);
30+
}
31+
} catch {
32+
// Error is already set in the hook
33+
}
34+
};
35+
36+
const handleUninstall = (id: string, name: string) => {
37+
Alert.alert(
38+
"Uninstall Package",
39+
`Are you sure you want to uninstall "${name}"?`,
40+
[
41+
{ text: "Cancel", style: "cancel" },
42+
{
43+
text: "Uninstall",
44+
style: "destructive",
45+
onPress: async () => {
46+
try {
47+
await uninstall(id);
48+
} catch {
49+
// Error is already set in the hook
50+
}
51+
},
52+
},
53+
],
54+
);
55+
};
56+
57+
return (
58+
<>
59+
<Stack.Screen options={{ title: "Packages" }} />
60+
<ScrollView className="flex-1 bg-background">
61+
{isLoading && !packages.length ? (
62+
<View className="flex-1 items-center justify-center py-20">
63+
<ActivityIndicator size="large" color="#1e40af" />
64+
</View>
65+
) : error ? (
66+
<View className="flex-1 items-center justify-center px-6 py-20">
67+
<Text className="text-destructive text-center mb-4">
68+
{error.message}
69+
</Text>
70+
<TouchableOpacity
71+
onPress={refetch}
72+
className="bg-primary px-4 py-2 rounded-lg"
73+
>
74+
<Text className="text-primary-foreground font-medium">
75+
Retry
76+
</Text>
77+
</TouchableOpacity>
78+
</View>
79+
) : !packages.length ? (
80+
<View className="flex-1 items-center justify-center px-6 py-20">
81+
<Package size={48} color="#9ca3af" />
82+
<Text className="text-muted-foreground mt-4">
83+
No packages installed
84+
</Text>
85+
</View>
86+
) : (
87+
<View className="p-4 gap-3">
88+
{packages.map((pkg) => (
89+
<Card key={pkg.id}>
90+
<CardHeader>
91+
<View className="flex-row items-center justify-between">
92+
<View className="flex-row items-center gap-2 flex-1">
93+
<Package size={18} color="#1e40af" />
94+
<CardTitle>{pkg.label}</CardTitle>
95+
</View>
96+
<View className="flex-row items-center gap-3">
97+
<TouchableOpacity
98+
onPress={() => handleToggle(pkg.id, pkg.enabled)}
99+
>
100+
{pkg.enabled ? (
101+
<ToggleRight size={24} color="#16a34a" />
102+
) : (
103+
<ToggleLeft size={24} color="#9ca3af" />
104+
)}
105+
</TouchableOpacity>
106+
<TouchableOpacity
107+
onPress={() => handleUninstall(pkg.id, pkg.label)}
108+
>
109+
<Trash2 size={18} color="#dc2626" />
110+
</TouchableOpacity>
111+
</View>
112+
</View>
113+
</CardHeader>
114+
<CardContent>
115+
{pkg.description && (
116+
<Text className="text-sm text-muted-foreground mb-1">
117+
{pkg.description}
118+
</Text>
119+
)}
120+
<View className="flex-row items-center gap-2">
121+
{pkg.version && (
122+
<Text className="text-xs text-muted-foreground">
123+
v{pkg.version}
124+
</Text>
125+
)}
126+
<View
127+
className={`px-2 py-0.5 rounded-full ${
128+
pkg.enabled ? "bg-green-100" : "bg-gray-100"
129+
}`}
130+
>
131+
<Text
132+
className={`text-xs ${
133+
pkg.enabled
134+
? "text-green-700"
135+
: "text-gray-500"
136+
}`}
137+
>
138+
{pkg.enabled ? "Enabled" : "Disabled"}
139+
</Text>
140+
</View>
141+
</View>
142+
</CardContent>
143+
</Card>
144+
))}
145+
</View>
146+
)}
147+
</ScrollView>
148+
</>
149+
);
150+
}

app/(app)/page/[id].tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React, { useEffect, useState } from "react";
2+
import { View, Text } from "react-native";
3+
import { useLocalSearchParams, Stack } from "expo-router";
4+
import { useClient } from "@objectstack/client-react";
5+
import { PageRenderer } from "~/components/renderers/PageRenderer";
6+
import {
7+
validatePageSchema,
8+
type PageSchema,
9+
} from "~/lib/page-renderer";
10+
11+
/**
12+
* Dynamic SDUI page route.
13+
* Fetches a PageSchema by ID from the server and renders it
14+
* using the PageRenderer component.
15+
*
16+
* Route: app/(app)/page/[id].tsx
17+
*/
18+
export default function SDUIPageScreen() {
19+
const { id } = useLocalSearchParams<{ id: string }>();
20+
const client = useClient();
21+
const [schema, setSchema] = useState<PageSchema | null>(null);
22+
const [isLoading, setIsLoading] = useState(true);
23+
const [error, setError] = useState<Error | null>(null);
24+
25+
useEffect(() => {
26+
if (!id) return;
27+
let cancelled = false;
28+
29+
async function fetchPage() {
30+
setIsLoading(true);
31+
setError(null);
32+
try {
33+
const result = await client.meta.getItem("page", id!);
34+
if (cancelled) return;
35+
const validated = validatePageSchema(result);
36+
if (!validated) {
37+
setError(new Error(`Invalid page schema for "${id}"`));
38+
} else {
39+
setSchema(validated);
40+
}
41+
} catch (err) {
42+
if (cancelled) return;
43+
setError(
44+
err instanceof Error ? err : new Error("Failed to load page"),
45+
);
46+
} finally {
47+
if (!cancelled) setIsLoading(false);
48+
}
49+
}
50+
51+
fetchPage();
52+
return () => {
53+
cancelled = true;
54+
};
55+
}, [client, id]);
56+
57+
return (
58+
<>
59+
<Stack.Screen options={{ title: schema?.label ?? id ?? "Page" }} />
60+
{error && !isLoading ? (
61+
<View className="flex-1 items-center justify-center px-6">
62+
<Text className="text-destructive text-center">{error.message}</Text>
63+
</View>
64+
) : schema ? (
65+
<PageRenderer schema={schema} isLoading={isLoading} />
66+
) : (
67+
<PageRenderer
68+
schema={{ name: id ?? "", regions: [] }}
69+
isLoading={isLoading}
70+
error={error}
71+
/>
72+
)}
73+
</>
74+
);
75+
}

0 commit comments

Comments
 (0)