@thunder-stack/create-thunder-app 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/index.js +229 -0
  2. package/package.json +26 -0
  3. package/postpublish.js +14 -0
  4. package/prepublish.js +68 -0
  5. package/template/.env.example +13 -0
  6. package/template/README.md +1 -0
  7. package/template/THUNDER_STACK.md +120 -0
  8. package/template/client/assets/logo.svg +36 -0
  9. package/template/client/expo/App.test.tsx +20 -0
  10. package/template/client/expo/App.tsx +16 -0
  11. package/template/client/expo/app.json +30 -0
  12. package/template/client/expo/babel.config.js +12 -0
  13. package/template/client/expo/global.css +3 -0
  14. package/template/client/expo/jest.config.js +7 -0
  15. package/template/client/expo/metro.config.js +21 -0
  16. package/template/client/expo/package.json +33 -0
  17. package/template/client/expo/tsconfig.json +6 -0
  18. package/template/client/next/app/globals.css +14 -0
  19. package/template/client/next/app/layout.tsx +28 -0
  20. package/template/client/next/app/page.test.tsx +13 -0
  21. package/template/client/next/app/page.tsx +10 -0
  22. package/template/client/next/next-env.d.ts +5 -0
  23. package/template/client/next/next.config.js +21 -0
  24. package/template/client/next/package.json +39 -0
  25. package/template/client/next/playwright.config.ts +17 -0
  26. package/template/client/next/postcss.config.js +6 -0
  27. package/template/client/next/tsconfig.json +30 -0
  28. package/template/client/next/vitest.config.ts +11 -0
  29. package/template/client/next/vitest.setup.ts +1 -0
  30. package/template/client/shared/index.ts +2 -0
  31. package/template/client/shared/package.json +24 -0
  32. package/template/client/shared/src/components/Button.tsx +38 -0
  33. package/template/client/shared/src/components/Card.tsx +17 -0
  34. package/template/client/shared/src/features/auth/login.tsx +139 -0
  35. package/template/client/shared/src/features/dashboard/index.tsx +232 -0
  36. package/template/client/shared/src/features/home/screen.tsx +212 -0
  37. package/template/client/shared/src/features/management/roles.tsx +356 -0
  38. package/template/client/shared/src/features/management/users.tsx +245 -0
  39. package/template/client/shared/src/nativewind-env.d.ts +1 -0
  40. package/template/client/shared/src/utils/auth.ts +21 -0
  41. package/template/client/shared/tailwind.config.js +19 -0
  42. package/template/client/shared/tsconfig.json +9 -0
  43. package/template/package.json +47 -0
  44. package/template/packages/tsconfig/base.json +20 -0
  45. package/template/packages/tsconfig/package.json +11 -0
  46. package/template/pnpm-workspace.yaml +4 -0
  47. package/template/scripts/clean.js +56 -0
  48. package/template/server/db/drizzle.config.ts +10 -0
  49. package/template/server/db/migrations/0000_loving_mindworm.sql +86 -0
  50. package/template/server/db/migrations/meta/0000_snapshot.json +549 -0
  51. package/template/server/db/migrations/meta/_journal.json +13 -0
  52. package/template/server/db/package.json +28 -0
  53. package/template/server/db/src/index.ts +16 -0
  54. package/template/server/db/src/migrate.ts +28 -0
  55. package/template/server/db/src/schema/auth.ts +73 -0
  56. package/template/server/db/src/schema/index.ts +2 -0
  57. package/template/server/db/src/schema/rbac.ts +72 -0
  58. package/template/server/db/src/schema.ts +1 -0
  59. package/template/server/db/src/seed.ts +204 -0
  60. package/template/server/db/src/services/rbac.ts +144 -0
  61. package/template/server/db/src/services/user.ts +34 -0
  62. package/template/server/db/src/types/index.ts +34 -0
  63. package/template/server/db/tsconfig.json +8 -0
  64. package/template/server/hono/package.json +22 -0
  65. package/template/server/hono/src/auth.ts +29 -0
  66. package/template/server/hono/src/index.ts +215 -0
  67. package/template/server/hono/tsconfig.json +8 -0
  68. package/template/server/hono/wrangler.toml +12 -0
  69. package/template/turbo.json +68 -0
@@ -0,0 +1,212 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { View as MotiView } from "moti";
3
+ import { View, Text, ActivityIndicator, TouchableOpacity, Platform, Image } from "react-native";
4
+ import { authClient } from "../../utils/auth";
5
+ import { LoginScreen } from "../auth/login";
6
+ import { DashboardView } from "../dashboard";
7
+ import { UserManagementView } from "../management/users";
8
+ import { RolesManagementView } from "../management/roles";
9
+
10
+ const logoSource = Platform.select({
11
+ web: { uri: "/logo.svg" },
12
+ default: require("../../../../assets/logo.png"),
13
+ });
14
+
15
+ export function HomeScreen() {
16
+ const { data: session, isPending: sessionPending } = authClient.useSession();
17
+ const [me, setMe] = useState<{ roles: string[]; permissions: string[] } | null>(null);
18
+ const [mePending, setMePending] = useState(false);
19
+ const [activeTab, setActiveTab] = useState("dashboard"); // "dashboard" | "users" | "roles"
20
+
21
+ const fetchProfile = async () => {
22
+ if (!session) return;
23
+ setMePending(true);
24
+ try {
25
+ const res = await fetch(`${authClient.baseURL}/api/me`, {
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ },
29
+ });
30
+ const data = await res.json();
31
+ if (!data.error) {
32
+ setMe({
33
+ roles: data.roles,
34
+ permissions: data.permissions,
35
+ });
36
+ }
37
+ } catch (err) {
38
+ console.error("Failed to load profile details:", err);
39
+ } finally {
40
+ setMePending(false);
41
+ }
42
+ };
43
+
44
+ useEffect(() => {
45
+ if (session) {
46
+ fetchProfile();
47
+ } else {
48
+ setMe(null);
49
+ }
50
+ }, [session]);
51
+
52
+ const handleSignOut = async () => {
53
+ try {
54
+ await authClient.signOut();
55
+ setMe(null);
56
+ setActiveTab("dashboard");
57
+ } catch (error) {
58
+ console.error("Sign out failed:", error);
59
+ }
60
+ };
61
+
62
+ if (sessionPending || (session && mePending)) {
63
+ return (
64
+ <View className="flex-1 items-center justify-center bg-slate-950">
65
+ <ActivityIndicator size="large" color="#3b82f6" />
66
+ </View>
67
+ );
68
+ }
69
+
70
+ // If not authenticated, render LoginScreen
71
+ if (!session) {
72
+ return (
73
+ <View className="flex-1 items-center justify-center bg-slate-950 px-6 py-12 relative overflow-hidden">
74
+ {/* Glowing visual background lights */}
75
+ <View className="absolute top-1/4 left-1/4 w-80 h-80 bg-blue-600/10 rounded-full blur-3xl" />
76
+ <View className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-indigo-600/10 rounded-full blur-3xl" />
77
+
78
+ {/* Main Branding Header */}
79
+ <View className="items-center mb-6">
80
+ <Image
81
+ source={logoSource}
82
+ style={{ width: 80, height: 80, marginBottom: 16, borderRadius: 16 }}
83
+ resizeMode="contain"
84
+ />
85
+ <Text className="text-5xl font-black tracking-tight text-white mb-2">
86
+ THUNDER<Text className="text-yellow-500">.stack</Text>
87
+ </Text>
88
+ <Text className="text-slate-400 text-sm font-medium text-center max-w-sm leading-6">
89
+ Unified Solito Interface • Cross-Platform Web & Mobile
90
+ </Text>
91
+ </View>
92
+
93
+ <LoginScreen onSuccess={fetchProfile} />
94
+ </View>
95
+ );
96
+ }
97
+
98
+ // Permissions mapping helper
99
+ const hasPermission = (perm: string) => {
100
+ return me?.permissions?.includes(perm) ?? false;
101
+ };
102
+
103
+ return (
104
+ <View className="flex-1 bg-slate-950 flex-row flex-wrap">
105
+ {/* Dynamic Visual Glowing Background Lights */}
106
+ <View className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-600/5 rounded-full blur-3xl" />
107
+ <View className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-indigo-600/5 rounded-full blur-3xl" />
108
+
109
+ {/* Navigation Layout Header/Sidebar */}
110
+ {/* On Web/Desktop we can render a sidebar, on mobile we render a top header */}
111
+ <View className="w-full md:w-64 bg-slate-900/40 border-b md:border-b-0 md:border-r border-slate-900 p-6 flex-col justify-between">
112
+ <View>
113
+ {/* Logo Branding */}
114
+ <View className="mb-8 flex-row items-center justify-between">
115
+ <View className="flex-row items-center gap-3">
116
+ <Image
117
+ source={logoSource}
118
+ style={{ width: 32, height: 32, borderRadius: 8 }}
119
+ resizeMode="contain"
120
+ />
121
+ <Text className="text-2xl font-black text-white tracking-tight">
122
+ THUNDER<Text className="text-yellow-500">.stack</Text>
123
+ </Text>
124
+ </View>
125
+ <View className="bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
126
+ <Text className="text-emerald-400 text-[10px] font-bold uppercase tracking-wider">Live</Text>
127
+ </View>
128
+ </View>
129
+
130
+ {/* Navigation Items */}
131
+ <View className="gap-2">
132
+ <TouchableOpacity
133
+ onPress={() => setActiveTab("dashboard")}
134
+ className={`w-full px-4 py-3 rounded-xl flex-row items-center gap-3 ${
135
+ activeTab === "dashboard" ? "bg-blue-600/10 border border-blue-500/20" : "border border-transparent"
136
+ }`}
137
+ >
138
+ <Text className={`font-bold text-sm ${activeTab === "dashboard" ? "text-blue-400" : "text-slate-400"}`}>
139
+ 📊 Dashboard
140
+ </Text>
141
+ </TouchableOpacity>
142
+
143
+ {hasPermission("manage:user") && (
144
+ <TouchableOpacity
145
+ onPress={() => setActiveTab("users")}
146
+ className={`w-full px-4 py-3 rounded-xl flex-row items-center gap-3 ${
147
+ activeTab === "users" ? "bg-blue-600/10 border border-blue-500/20" : "border border-transparent"
148
+ }`}
149
+ >
150
+ <Text className={`font-bold text-sm ${activeTab === "users" ? "text-blue-400" : "text-slate-400"}`}>
151
+ 👥 Users
152
+ </Text>
153
+ </TouchableOpacity>
154
+ )}
155
+
156
+ {(hasPermission("manage:role") || hasPermission("manage:role-perm")) && (
157
+ <TouchableOpacity
158
+ onPress={() => setActiveTab("roles")}
159
+ className={`w-full px-4 py-3 rounded-xl flex-row items-center gap-3 ${
160
+ activeTab === "roles" ? "bg-blue-600/10 border border-blue-500/20" : "border border-transparent"
161
+ }`}
162
+ >
163
+ <Text className={`font-bold text-sm ${activeTab === "roles" ? "text-blue-400" : "text-slate-400"}`}>
164
+ 🔑 Roles & Perms
165
+ </Text>
166
+ </TouchableOpacity>
167
+ )}
168
+ </View>
169
+ </View>
170
+
171
+ {/* User Card & Logout inside Sidebar */}
172
+ <View className="mt-8 border-t border-slate-900 pt-6 gap-4">
173
+ <View className="flex-row items-center gap-3">
174
+ <View className="w-9 h-9 rounded-full bg-slate-800 border border-slate-700 items-center justify-center">
175
+ <Text className="text-white text-sm font-bold">
176
+ {session?.user?.name?.charAt(0) ?? "U"}
177
+ </Text>
178
+ </View>
179
+ <View className="flex-1">
180
+ <Text className="text-white text-xs font-bold leading-4 truncate max-w-[140px]" numberOfLines={1}>
181
+ {session?.user?.name}
182
+ </Text>
183
+ <Text className="text-slate-500 text-[10px] truncate max-w-[140px]" numberOfLines={1}>
184
+ {session?.user?.email}
185
+ </Text>
186
+ </View>
187
+ </View>
188
+
189
+ <TouchableOpacity
190
+ onPress={handleSignOut}
191
+ className="w-full bg-slate-950/40 border border-slate-800/80 py-2.5 rounded-xl items-center"
192
+ >
193
+ <Text className="text-slate-400 text-xs font-bold">Sign Out</Text>
194
+ </TouchableOpacity>
195
+ </View>
196
+ </View>
197
+
198
+ {/* Main Content Area */}
199
+ <View className="flex-1 min-w-[280px]">
200
+ {activeTab === "dashboard" && (
201
+ <DashboardView
202
+ user={session.user}
203
+ roles={me?.roles ?? []}
204
+ permissions={me?.permissions ?? []}
205
+ />
206
+ )}
207
+ {activeTab === "users" && hasPermission("manage:user") && <UserManagementView />}
208
+ {activeTab === "roles" && (hasPermission("manage:role") || hasPermission("manage:role-perm")) && <RolesManagementView />}
209
+ </View>
210
+ </View>
211
+ );
212
+ }
@@ -0,0 +1,356 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { View as MotiView } from "moti";
3
+ import { View, Text, ActivityIndicator, ScrollView, RefreshControl, TextInput, TouchableOpacity } from "react-native";
4
+ import { Card } from "../../components/Card";
5
+ import { authClient } from "../../utils/auth";
6
+
7
+ function RolesSkeleton() {
8
+ return (
9
+ <ScrollView className="flex-1">
10
+ <View className="p-6 gap-8">
11
+ <View>
12
+ <View className="h-3 w-20 bg-slate-800 rounded mb-2" />
13
+ <View className="h-8 w-48 bg-slate-800 rounded" />
14
+ </View>
15
+
16
+ {/* Create Role Form Skeleton */}
17
+ <MotiView
18
+ from={{ opacity: 0.3 }}
19
+ animate={{ opacity: 0.7 }}
20
+ transition={{
21
+ type: "timing",
22
+ duration: 900,
23
+ loop: true,
24
+ }}
25
+ className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border gap-4"
26
+ >
27
+ <View className="h-5 w-40 bg-slate-800 rounded mb-2" />
28
+ <View className="h-10 w-full bg-slate-800 rounded-xl" />
29
+ <View className="h-10 w-full bg-slate-800 rounded-xl" />
30
+ <View className="h-10 w-full bg-slate-800 rounded-xl" />
31
+ </MotiView>
32
+
33
+ {/* Role Cards List Skeleton */}
34
+ <View className="gap-6">
35
+ <View className="h-6 w-44 bg-slate-800 rounded mb-2" />
36
+ {[1, 2].map((i) => (
37
+ <MotiView
38
+ key={i}
39
+ from={{ opacity: 0.3 }}
40
+ animate={{ opacity: 0.7 }}
41
+ transition={{
42
+ type: "timing",
43
+ duration: 900,
44
+ loop: true,
45
+ delay: i * 200,
46
+ }}
47
+ className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border gap-4 min-h-[160px]"
48
+ >
49
+ <View className="flex-row justify-between items-start">
50
+ <View className="gap-2">
51
+ <View className="h-5 w-32 bg-slate-800 rounded" />
52
+ <View className="h-3.5 w-64 bg-slate-800 rounded" />
53
+ </View>
54
+ </View>
55
+ <View className="mt-2 border-t border-slate-800/60 pt-4 gap-3">
56
+ <View className="h-3 w-36 bg-slate-800 rounded" />
57
+ <View className="flex-row gap-2 flex-wrap">
58
+ {[1, 2, 3, 4].map((j) => (
59
+ <View key={j} className="w-24 h-8 bg-slate-800 rounded-full" />
60
+ ))}
61
+ </View>
62
+ </View>
63
+ </MotiView>
64
+ ))}
65
+ </View>
66
+ </View>
67
+ </ScrollView>
68
+ );
69
+ }
70
+
71
+ export function RolesManagementView() {
72
+ const [roles, setRoles] = useState<any[]>([]);
73
+ const [permissions, setPermissions] = useState<any[]>([]);
74
+ const [loading, setLoading] = useState(true);
75
+ const [refreshing, setRefreshing] = useState(false);
76
+ const [newRoleName, setNewRoleName] = useState("");
77
+ const [newRoleDesc, setNewRoleDesc] = useState("");
78
+ const [actionLoading, setActionLoading] = useState(false);
79
+ const [error, setError] = useState("");
80
+
81
+ const fetchData = async () => {
82
+ try {
83
+ const rolesRes = await fetch(`${authClient.baseURL}/api/management/roles`);
84
+ const rolesData = await rolesRes.json();
85
+
86
+ const permsRes = await fetch(`${authClient.baseURL}/api/management/permissions`);
87
+ const permsData = await permsRes.json();
88
+
89
+ if (rolesRes.status === 403 || permsRes.status === 403) {
90
+ setError("Permission denied.");
91
+ } else {
92
+ setRoles(rolesData);
93
+ setPermissions(permsData);
94
+ }
95
+ } catch (err) {
96
+ console.error("Failed to load roles and permissions:", err);
97
+ setError("Failed to fetch data.");
98
+ } finally {
99
+ setLoading(false);
100
+ setRefreshing(false);
101
+ }
102
+ };
103
+
104
+ useEffect(() => {
105
+ fetchData();
106
+ }, []);
107
+
108
+ const handleCreateRole = async () => {
109
+ if (!newRoleName) {
110
+ setError("Role name is required.");
111
+ return;
112
+ }
113
+
114
+ setActionLoading(true);
115
+ setError("");
116
+
117
+ try {
118
+ const res = await fetch(`${authClient.baseURL}/api/management/roles`, {
119
+ method: "POST",
120
+ headers: {
121
+ "Content-Type": "application/json",
122
+ },
123
+ body: JSON.stringify({ name: newRoleName, description: newRoleDesc }),
124
+ });
125
+
126
+ if (!res.ok) {
127
+ const errorData = await res.json();
128
+ throw new Error(errorData.error || "Failed to create role.");
129
+ }
130
+
131
+ setNewRoleName("");
132
+ setNewRoleDesc("");
133
+ await fetchData();
134
+ } catch (err: any) {
135
+ setError(err?.message || "An error occurred during role creation.");
136
+ } finally {
137
+ setActionLoading(false);
138
+ }
139
+ };
140
+
141
+ const handleDeleteRole = async (roleId: string) => {
142
+ if (roleId === "role_admin" || roleId === "role_user") {
143
+ setError("Default system roles (Admin/User) cannot be deleted.");
144
+ return;
145
+ }
146
+
147
+ setActionLoading(true);
148
+ setError("");
149
+
150
+ try {
151
+ const res = await fetch(`${authClient.baseURL}/api/management/roles/${roleId}`, {
152
+ method: "DELETE",
153
+ });
154
+
155
+ if (!res.ok) {
156
+ const errorData = await res.json();
157
+ throw new Error(errorData.error || "Failed to delete role.");
158
+ }
159
+
160
+ await fetchData();
161
+ } catch (err: any) {
162
+ setError(err?.message || "Failed to delete role.");
163
+ } finally {
164
+ setActionLoading(false);
165
+ }
166
+ };
167
+
168
+ const handleTogglePermission = async (roleId: string, permissionId: string, currentlyAssigned: boolean) => {
169
+ setActionLoading(true);
170
+ setError("");
171
+
172
+ try {
173
+ const url = `${authClient.baseURL}/api/management/roles/${roleId}/permissions${
174
+ currentlyAssigned ? `/${permissionId}` : ""
175
+ }`;
176
+ const method = currentlyAssigned ? "DELETE" : "POST";
177
+
178
+ const res = await fetch(url, {
179
+ method,
180
+ headers: {
181
+ "Content-Type": "application/json",
182
+ },
183
+ body: method === "POST" ? JSON.stringify({ permissionId }) : undefined,
184
+ });
185
+
186
+ if (!res.ok) {
187
+ const errorData = await res.json();
188
+ throw new Error(errorData.error || "Failed to update permission mapping.");
189
+ }
190
+
191
+ await fetchData();
192
+ } catch (err: any) {
193
+ setError(err?.message || "Failed to toggle permission.");
194
+ } finally {
195
+ setActionLoading(false);
196
+ }
197
+ };
198
+
199
+ const onRefresh = () => {
200
+ setRefreshing(true);
201
+ fetchData();
202
+ };
203
+
204
+ if (loading) {
205
+ return <RolesSkeleton />;
206
+ }
207
+
208
+ return (
209
+ <ScrollView
210
+ className="flex-1"
211
+ refreshControl={
212
+ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#3b82f6" />
213
+ }
214
+ >
215
+ <View className="p-6 gap-8">
216
+ {/* Title */}
217
+ <MotiView
218
+ from={{ opacity: 0, translateY: -10 }}
219
+ animate={{ opacity: 1, translateY: 0 }}
220
+ transition={{ type: "timing", duration: 400 }}
221
+ >
222
+ <Text className="text-slate-400 text-xs font-bold uppercase tracking-widest mb-1">
223
+ Administration
224
+ </Text>
225
+ <Text className="text-white text-3xl font-black tracking-tight">
226
+ Roles & Permissions
227
+ </Text>
228
+ </MotiView>
229
+
230
+ {error ? (
231
+ <View className="p-4 bg-red-950/40 border border-red-500/20 rounded-xl">
232
+ <Text className="text-red-400 text-xs font-semibold text-center">{error}</Text>
233
+ </View>
234
+ ) : null}
235
+
236
+ {/* Create Role Form */}
237
+ <MotiView
238
+ from={{ opacity: 0, scale: 0.98 }}
239
+ animate={{ opacity: 1, scale: 1 }}
240
+ transition={{ type: "timing", duration: 400 }}
241
+ >
242
+ <Card className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border">
243
+ <Text className="text-white text-lg font-bold mb-4">Create New Custom Role</Text>
244
+ <View className="gap-4">
245
+ <View>
246
+ <Text className="text-slate-400 text-xs font-semibold uppercase tracking-wider mb-2">
247
+ Role Name
248
+ </Text>
249
+ <TextInput
250
+ className="w-full bg-slate-950/60 border border-slate-800 text-white rounded-xl px-4 py-3 text-sm focus:border-blue-500"
251
+ placeholder="e.g. Editor"
252
+ placeholderTextColor="#64748b"
253
+ value={newRoleName}
254
+ onChangeText={setNewRoleName}
255
+ />
256
+ </View>
257
+
258
+ <View>
259
+ <Text className="text-slate-400 text-xs font-semibold uppercase tracking-wider mb-2">
260
+ Description
261
+ </Text>
262
+ <TextInput
263
+ className="w-full bg-slate-950/60 border border-slate-800 text-white rounded-xl px-4 py-3 text-sm focus:border-blue-500"
264
+ placeholder="Role description here..."
265
+ placeholderTextColor="#64748b"
266
+ value={newRoleDesc}
267
+ onChangeText={setNewRoleDesc}
268
+ />
269
+ </View>
270
+
271
+ {actionLoading ? (
272
+ <ActivityIndicator size="small" color="#3b82f6" className="my-2" />
273
+ ) : (
274
+ <TouchableOpacity
275
+ onPress={handleCreateRole}
276
+ className="w-full bg-blue-600 border border-blue-500 rounded-xl py-3 items-center"
277
+ >
278
+ <Text className="text-white font-extrabold text-sm">Create Role</Text>
279
+ </TouchableOpacity>
280
+ )}
281
+ </View>
282
+ </Card>
283
+ </MotiView>
284
+
285
+ {/* Roles List & Matrix */}
286
+ <View className="gap-6">
287
+ <Text className="text-white text-xl font-bold">System Role Mappings</Text>
288
+
289
+ {roles.map((roleItem, index) => {
290
+ const rolePerms = roleItem.permissions.map((p: any) => p.name);
291
+ const isSystemRole = roleItem.id === "role_admin" || roleItem.id === "role_user";
292
+
293
+ return (
294
+ <MotiView
295
+ key={roleItem.id}
296
+ from={{ opacity: 0, translateY: 15 }}
297
+ animate={{ opacity: 1, translateY: 0 }}
298
+ transition={{ type: "timing", duration: 400, delay: index * 50 }}
299
+ >
300
+ <Card className="bg-slate-900/40 border-slate-800 p-6 rounded-2xl border gap-4">
301
+ <View className="flex-row justify-between items-start">
302
+ <View className="flex-1">
303
+ <Text className="text-white text-lg font-extrabold">{roleItem.name}</Text>
304
+ <Text className="text-slate-400 text-xs mt-1">{roleItem.description}</Text>
305
+ </View>
306
+ {!isSystemRole && (
307
+ <TouchableOpacity
308
+ onPress={() => handleDeleteRole(roleItem.id)}
309
+ disabled={actionLoading}
310
+ className="bg-red-950/20 border border-red-500/20 px-3 py-1 rounded-md"
311
+ >
312
+ <Text className="text-red-400 text-xs font-bold">Delete</Text>
313
+ </TouchableOpacity>
314
+ )}
315
+ </View>
316
+
317
+ <View className="mt-2 border-t border-slate-800/60 pt-4">
318
+ <Text className="text-slate-500 text-xs font-bold uppercase tracking-wider mb-3">
319
+ Permission Assignments
320
+ </Text>
321
+
322
+ <View className="flex-row flex-wrap gap-2">
323
+ {permissions.map((perm) => {
324
+ const isAssigned = rolePerms.includes(perm.name);
325
+ return (
326
+ <TouchableOpacity
327
+ key={perm.id}
328
+ onPress={() => handleTogglePermission(roleItem.id, perm.id, isAssigned)}
329
+ disabled={actionLoading}
330
+ className={`flex-row items-center gap-1.5 px-3 py-1.5 rounded-full border ${
331
+ isAssigned
332
+ ? "bg-emerald-500/10 border-emerald-500/30"
333
+ : "bg-slate-950/40 border-slate-800"
334
+ }`}
335
+ >
336
+ <Text
337
+ className={`text-xs font-semibold ${
338
+ isAssigned ? "text-emerald-400" : "text-slate-400"
339
+ }`}
340
+ >
341
+ {isAssigned ? "✓" : "+"} {perm.name}
342
+ </Text>
343
+ </TouchableOpacity>
344
+ );
345
+ })}
346
+ </View>
347
+ </View>
348
+ </Card>
349
+ </MotiView>
350
+ );
351
+ })}
352
+ </View>
353
+ </View>
354
+ </ScrollView>
355
+ );
356
+ }