@everystack/admin 0.1.0

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 (38) hide show
  1. package/README.md +282 -0
  2. package/package.json +53 -0
  3. package/src/AdminRoot.tsx +34 -0
  4. package/src/adapters/EverystackAdapter.ts +218 -0
  5. package/src/adapters/types.ts +78 -0
  6. package/src/components/AvatarFieldInput.tsx +100 -0
  7. package/src/components/ImageDisplay.tsx +57 -0
  8. package/src/components/ImageFieldInput.tsx +140 -0
  9. package/src/components/dashboard/Brush.tsx +139 -0
  10. package/src/components/dashboard/DimensionBars.tsx +67 -0
  11. package/src/components/dashboard/DimensionDonut.tsx +98 -0
  12. package/src/components/dashboard/EventTable.tsx +248 -0
  13. package/src/components/dashboard/StatDials.tsx +67 -0
  14. package/src/components/dashboard/TimeHistogram.tsx +158 -0
  15. package/src/context.ts +75 -0
  16. package/src/hooks/useAdminConfig.ts +22 -0
  17. package/src/hooks/useAdminData.ts +132 -0
  18. package/src/hooks/useAggregates.ts +290 -0
  19. package/src/hooks/useRpc.ts +124 -0
  20. package/src/hooks/useStorageConfig.ts +16 -0
  21. package/src/index.ts +138 -0
  22. package/src/presets/twitter.ts +267 -0
  23. package/src/providers/AdminConfigProvider.tsx +18 -0
  24. package/src/providers/DataAdapterProvider.tsx +18 -0
  25. package/src/rpc/resource-dashboard-rpc.ts +416 -0
  26. package/src/screens/AdminDashboard.tsx +217 -0
  27. package/src/screens/AdminLayout.tsx +97 -0
  28. package/src/screens/ResourceCreate.tsx +243 -0
  29. package/src/screens/ResourceDashboard.tsx +278 -0
  30. package/src/screens/ResourceEdit.tsx +283 -0
  31. package/src/screens/ResourceList.tsx +153 -0
  32. package/src/screens/ResourceShow.tsx +171 -0
  33. package/src/screens/index.ts +14 -0
  34. package/src/types.ts +358 -0
  35. package/src/utils/naming.ts +105 -0
  36. package/src/utils/pick-image-web.ts +81 -0
  37. package/src/utils/resolve-image-url.ts +43 -0
  38. package/src/utils/upload-file.ts +93 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * ResourceEdit
3
+ *
4
+ * Edit form for an existing resource record.
5
+ * Pre-populates form with existing data, submits via adapter.update().
6
+ */
7
+
8
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
9
+ import { View, Text, ScrollView } from 'react-native';
10
+ import { useAdminConfig } from '../hooks/useAdminConfig';
11
+ import { useDataAdapter } from '../hooks/useAdminData';
12
+ import { FormField, TextInput, SelectInput, SwitchInput, Button, LoadingState, ErrorState } from '@everystack/ui';
13
+ import { ImageFieldInput } from '../components/ImageFieldInput';
14
+ import { AvatarFieldInput } from '../components/AvatarFieldInput';
15
+ import type { FieldDefinition, DataRecord } from '../types';
16
+
17
+ export interface ResourceEditProps {
18
+ /** Resource name */
19
+ resource: string;
20
+ /** Record ID */
21
+ id: string | number;
22
+ /** Called after successful update */
23
+ onSuccess?: (record: Record<string, unknown>) => void;
24
+ /** Called when the cancel button is pressed */
25
+ onCancel?: () => void;
26
+ }
27
+
28
+ export function ResourceEdit({ resource, id, onSuccess, onCancel }: ResourceEditProps): React.ReactElement {
29
+ const config = useAdminConfig();
30
+ const adapter = useDataAdapter();
31
+ const resourceConfig = config.resources.find(r => r.name === resource);
32
+
33
+ const [record, setRecord] = useState<DataRecord | null>(null);
34
+ const [formData, setFormData] = useState<Record<string, unknown>>({});
35
+ const [errors, setErrors] = useState<Record<string, string>>({});
36
+ const [loading, setLoading] = useState(true);
37
+ const [submitting, setSubmitting] = useState(false);
38
+ const [fetchError, setFetchError] = useState<Error | null>(null);
39
+ const [submitError, setSubmitError] = useState<string | null>(null);
40
+
41
+ const fields = useMemo(() => {
42
+ if (!resourceConfig) return [];
43
+ const fieldNames = resourceConfig.editFields ?? resourceConfig.fields.map(f => f.name);
44
+ const fieldMap = new Map(resourceConfig.fields.map(f => [f.name, f]));
45
+ return fieldNames
46
+ .map(name => fieldMap.get(name))
47
+ .filter((f): f is FieldDefinition => f != null)
48
+ .filter(f => f.name !== (resourceConfig.primaryKey ?? 'id'));
49
+ }, [resourceConfig]);
50
+
51
+ // Fetch existing record
52
+ const fetchRecord = useCallback(async () => {
53
+ setLoading(true);
54
+ setFetchError(null);
55
+ try {
56
+ const result = await adapter.fetchOne(resource, id);
57
+ setRecord(result);
58
+ // Pre-populate form data with existing values
59
+ const initial: Record<string, unknown> = {};
60
+ for (const field of fields) {
61
+ initial[field.name] = result[field.name];
62
+ }
63
+ setFormData(initial);
64
+ } catch (err) {
65
+ setFetchError(err instanceof Error ? err : new Error(String(err)));
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ }, [adapter, resource, id, fields]);
70
+
71
+ useEffect(() => {
72
+ fetchRecord();
73
+ }, [fetchRecord]);
74
+
75
+ const setFieldValue = useCallback((name: string, value: unknown) => {
76
+ setFormData(prev => ({ ...prev, [name]: value }));
77
+ setErrors(prev => {
78
+ const next = { ...prev };
79
+ delete next[name];
80
+ return next;
81
+ });
82
+ }, []);
83
+
84
+ const validate = useCallback((): boolean => {
85
+ const newErrors: Record<string, string> = {};
86
+ for (const field of fields) {
87
+ const value = formData[field.name];
88
+ if (field.validation?.required && (value == null || value === '')) {
89
+ newErrors[field.name] = field.validation.message ?? `${field.label ?? field.name} is required`;
90
+ }
91
+ }
92
+ setErrors(newErrors);
93
+ return Object.keys(newErrors).length === 0;
94
+ }, [fields, formData]);
95
+
96
+ const handleSubmit = useCallback(async () => {
97
+ if (!validate()) return;
98
+
99
+ setSubmitting(true);
100
+ setSubmitError(null);
101
+
102
+ try {
103
+ const updated = await adapter.update(resource, id, formData);
104
+ onSuccess?.(updated);
105
+ } catch (err) {
106
+ setSubmitError(err instanceof Error ? err.message : String(err));
107
+ } finally {
108
+ setSubmitting(false);
109
+ }
110
+ }, [adapter, resource, id, formData, validate, onSuccess]);
111
+
112
+ if (!resourceConfig) {
113
+ return <ErrorState message={`Resource "${resource}" not found in admin configuration`} />;
114
+ }
115
+
116
+ if (loading) {
117
+ return <LoadingState message="Loading record..." />;
118
+ }
119
+
120
+ if (fetchError) {
121
+ return <ErrorState message={fetchError.message} onRetry={fetchRecord} />;
122
+ }
123
+
124
+ if (!record) {
125
+ return <ErrorState message="Record not found" />;
126
+ }
127
+
128
+ const label = resourceConfig.label ?? resourceConfig.name;
129
+
130
+ return (
131
+ <ScrollView className="flex-1">
132
+ {/* Header */}
133
+ <View className="flex-row items-center justify-between px-4 py-4">
134
+ <Text className="text-xl font-bold text-gray-900">
135
+ Edit {label} #{String(id)}
136
+ </Text>
137
+ <View className="flex-row gap-2">
138
+ {onCancel && (
139
+ <Button label="Cancel" variant="ghost" onPress={onCancel} />
140
+ )}
141
+ <Button label="Save" onPress={handleSubmit} loading={submitting} />
142
+ </View>
143
+ </View>
144
+
145
+ {/* Error */}
146
+ {submitError && (
147
+ <View className="mx-4 mb-4 bg-red-50 border border-red-200 rounded-lg px-4 py-3">
148
+ <Text className="text-sm text-red-700">{submitError}</Text>
149
+ </View>
150
+ )}
151
+
152
+ {/* Form fields */}
153
+ <View className="px-4">
154
+ {fields.map(field => (
155
+ <EditFieldInput
156
+ key={field.name}
157
+ field={field}
158
+ value={formData[field.name]}
159
+ error={errors[field.name]}
160
+ onChange={value => setFieldValue(field.name, value)}
161
+ />
162
+ ))}
163
+ </View>
164
+ </ScrollView>
165
+ );
166
+ }
167
+
168
+ // =============================================================================
169
+ // EditFieldInput — maps FieldDefinition.type to input component
170
+ // =============================================================================
171
+
172
+ interface EditFieldInputProps {
173
+ field: FieldDefinition;
174
+ value: unknown;
175
+ error?: string;
176
+ onChange: (value: unknown) => void;
177
+ }
178
+
179
+ function EditFieldInput({ field, value, error, onChange }: EditFieldInputProps): React.ReactElement {
180
+ const label = field.label ?? field.name;
181
+ const required = field.validation?.required;
182
+
183
+ switch (field.type) {
184
+ case 'boolean':
185
+ return (
186
+ <FormField label={label} error={error} required={required}>
187
+ <SwitchInput value={Boolean(value)} onValueChange={onChange} />
188
+ </FormField>
189
+ );
190
+
191
+ case 'enum':
192
+ return (
193
+ <FormField label={label} error={error} required={required}>
194
+ <SelectInput
195
+ value={String(value ?? '')}
196
+ options={(field.enumValues ?? []).map(v => ({ label: v, value: v }))}
197
+ onValueChange={onChange}
198
+ placeholder={`Select ${label.toLowerCase()}`}
199
+ />
200
+ </FormField>
201
+ );
202
+
203
+ case 'number':
204
+ return (
205
+ <FormField label={label} error={error} required={required}>
206
+ <TextInput
207
+ value={value != null ? String(value) : ''}
208
+ onChangeText={text => onChange(text ? Number(text) : null)}
209
+ keyboardType="numeric"
210
+ placeholder={`Enter ${label.toLowerCase()}`}
211
+ />
212
+ </FormField>
213
+ );
214
+
215
+ case 'email':
216
+ return (
217
+ <FormField label={label} error={error} required={required}>
218
+ <TextInput
219
+ value={String(value ?? '')}
220
+ onChangeText={onChange}
221
+ keyboardType="email-address"
222
+ placeholder={`Enter ${label.toLowerCase()}`}
223
+ />
224
+ </FormField>
225
+ );
226
+
227
+ case 'url':
228
+ return (
229
+ <FormField label={label} error={error} required={required}>
230
+ <TextInput
231
+ value={String(value ?? '')}
232
+ onChangeText={onChange}
233
+ keyboardType="url"
234
+ placeholder={`Enter ${label.toLowerCase()}`}
235
+ />
236
+ </FormField>
237
+ );
238
+
239
+ case 'json':
240
+ return (
241
+ <FormField label={label} error={error} required={required}>
242
+ <TextInput
243
+ value={typeof value === 'string' ? value : JSON.stringify(value ?? '', null, 2)}
244
+ onChangeText={text => {
245
+ try { onChange(JSON.parse(text)); } catch { onChange(text); }
246
+ }}
247
+ multiline
248
+ placeholder="{}"
249
+ />
250
+ </FormField>
251
+ );
252
+
253
+ case 'image':
254
+ if (field.renderer === 'avatar') {
255
+ return <AvatarFieldInput field={field} value={value} error={error} onChange={onChange} />;
256
+ }
257
+ return <ImageFieldInput field={field} value={value} error={error} onChange={onChange} />;
258
+
259
+ case 'file':
260
+ return (
261
+ <FormField label={label} error={error} required={required}>
262
+ <TextInput
263
+ value={String(value ?? '')}
264
+ onChangeText={onChange}
265
+ keyboardType="url"
266
+ placeholder="Enter file URL"
267
+ />
268
+ </FormField>
269
+ );
270
+
271
+ default:
272
+ return (
273
+ <FormField label={label} error={error} required={required}>
274
+ <TextInput
275
+ value={String(value ?? '')}
276
+ onChangeText={onChange}
277
+ placeholder={`Enter ${label.toLowerCase()}`}
278
+ multiline={field.type === 'string' && (field.validation?.max ?? 0) > 255}
279
+ />
280
+ </FormField>
281
+ );
282
+ }
283
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * ResourceList
3
+ *
4
+ * List view for a resource with sortable table, search/filter, and pagination.
5
+ * Wires useAdminData() to DataTable, FilterBar, and PaginationBar components.
6
+ */
7
+
8
+ import React, { useCallback, useMemo } from 'react';
9
+ import { View, Text } from 'react-native';
10
+ import { useAdminConfig } from '../hooks/useAdminConfig';
11
+ import { useAdminData } from '../hooks/useAdminData';
12
+ import { DataTable, FilterBar, PaginationBar, LoadingState, ErrorState, EmptyState, Button, type DataTableColumn } from '@everystack/ui';
13
+ import type { ResourceConfig, FieldDefinition } from '../types';
14
+
15
+ export interface ResourceListProps {
16
+ /** Resource name (matches ResourceConfig.name) */
17
+ resource: string;
18
+ /** Called when a row is pressed */
19
+ onRowPress?: (record: Record<string, unknown>) => void;
20
+ /** Called when the create button is pressed */
21
+ onCreatePress?: () => void;
22
+ }
23
+
24
+ export function ResourceList({ resource, onRowPress, onCreatePress }: ResourceListProps): React.ReactElement {
25
+ const config = useAdminConfig();
26
+ const resourceConfig = config.resources.find(r => r.name === resource);
27
+
28
+ const defaultSort = resourceConfig?.defaultSort;
29
+ const {
30
+ data,
31
+ total,
32
+ loading,
33
+ error,
34
+ refetch,
35
+ page,
36
+ perPage,
37
+ setPage,
38
+ setSort,
39
+ setFilter,
40
+ } = useAdminData(resource, {
41
+ sort: defaultSort?.field,
42
+ order: defaultSort?.order,
43
+ });
44
+
45
+ // Build columns from field definitions
46
+ const columns = useMemo(() => {
47
+ if (!resourceConfig) return [];
48
+ return fieldsToColumns(resourceConfig, defaultSort?.field, defaultSort?.order);
49
+ }, [resourceConfig, defaultSort?.field, defaultSort?.order]);
50
+
51
+ // Build filter chips from filterable fields
52
+ const filterChips = useMemo(() => {
53
+ if (!resourceConfig) return [];
54
+ return resourceConfig.fields
55
+ .filter(f => f.filterable)
56
+ .map(f => ({ key: f.name, label: f.label ?? f.name }));
57
+ }, [resourceConfig]);
58
+
59
+ const handleSort = useCallback((key: string) => {
60
+ setSort(key, 'asc');
61
+ }, [setSort]);
62
+
63
+ const handleSearch = useCallback((query: string) => {
64
+ if (!query) {
65
+ setFilter({});
66
+ return;
67
+ }
68
+ // Search across searchable fields using ilike
69
+ const searchableFields = resourceConfig?.fields.filter(f => f.searchable) ?? [];
70
+ if (searchableFields.length > 0) {
71
+ setFilter({ [searchableFields[0].name]: `ilike.*${query}*` });
72
+ }
73
+ }, [resourceConfig, setFilter]);
74
+
75
+ if (!resourceConfig) {
76
+ return (
77
+ <ErrorState message={`Resource "${resource}" not found in admin configuration`} />
78
+ );
79
+ }
80
+
81
+ const label = resourceConfig.labelPlural ?? resourceConfig.label ?? resourceConfig.name;
82
+ const canCreate = resourceConfig.actions?.create !== false;
83
+
84
+ return (
85
+ <View className="flex-1">
86
+ {/* Header */}
87
+ <View className="flex-row items-center justify-between px-4 py-4">
88
+ <Text className="text-xl font-bold text-gray-900">{label}</Text>
89
+ {canCreate && onCreatePress && (
90
+ <Button label="Create" onPress={onCreatePress} />
91
+ )}
92
+ </View>
93
+
94
+ {/* Filter bar */}
95
+ <FilterBar
96
+ placeholder={`Search ${label.toLowerCase()}...`}
97
+ onSearch={handleSearch}
98
+ filters={filterChips}
99
+ />
100
+
101
+ {/* Content */}
102
+ {loading ? (
103
+ <LoadingState message={`Loading ${label.toLowerCase()}...`} />
104
+ ) : error ? (
105
+ <ErrorState message={error.message} onRetry={refetch} />
106
+ ) : data.length === 0 ? (
107
+ <EmptyState
108
+ title={`No ${label.toLowerCase()}`}
109
+ message={`No ${label.toLowerCase()} found. ${canCreate ? 'Create one to get started.' : ''}`}
110
+ />
111
+ ) : (
112
+ <DataTable
113
+ columns={columns}
114
+ data={data}
115
+ onSort={handleSort}
116
+ onRowPress={onRowPress}
117
+ />
118
+ )}
119
+
120
+ {/* Pagination */}
121
+ {total > 0 && (
122
+ <PaginationBar
123
+ page={page}
124
+ perPage={perPage}
125
+ total={total}
126
+ onPageChange={setPage}
127
+ />
128
+ )}
129
+ </View>
130
+ );
131
+ }
132
+
133
+ // =============================================================================
134
+ // Helpers
135
+ // =============================================================================
136
+
137
+ function fieldsToColumns(
138
+ resourceConfig: ResourceConfig,
139
+ _sortField?: string,
140
+ _sortOrder?: 'asc' | 'desc'
141
+ ): DataTableColumn[] {
142
+ const fieldNames = resourceConfig.listFields ?? resourceConfig.fields.map(f => f.name);
143
+ const fieldMap = new Map(resourceConfig.fields.map(f => [f.name, f]));
144
+
145
+ return fieldNames
146
+ .map(name => fieldMap.get(name))
147
+ .filter((f): f is FieldDefinition => f != null)
148
+ .map(field => ({
149
+ key: field.name,
150
+ label: field.label ?? field.name,
151
+ sortable: field.sortable,
152
+ }));
153
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * ResourceShow
3
+ *
4
+ * Detail view for a single resource record.
5
+ * Fetches via adapter.fetchOne() and displays field values.
6
+ */
7
+
8
+ import React, { useState, useEffect, useCallback } from 'react';
9
+ import { View, Text, ScrollView } from 'react-native';
10
+ import { useAdminConfig } from '../hooks/useAdminConfig';
11
+ import { useDataAdapter } from '../hooks/useAdminData';
12
+ import { LoadingState, ErrorState, Button, Card, Divider } from '@everystack/ui';
13
+ import { ImageDisplay } from '../components/ImageDisplay';
14
+ import type { FieldDefinition, DataRecord } from '../types';
15
+
16
+ export interface ResourceShowProps {
17
+ /** Resource name */
18
+ resource: string;
19
+ /** Record ID */
20
+ id: string | number;
21
+ /** Called when the edit button is pressed */
22
+ onEditPress?: () => void;
23
+ /** Called when the delete button is pressed */
24
+ onDeletePress?: () => void;
25
+ /** Called when the back/list button is pressed */
26
+ onBackPress?: () => void;
27
+ }
28
+
29
+ export function ResourceShow({
30
+ resource,
31
+ id,
32
+ onEditPress,
33
+ onDeletePress,
34
+ onBackPress,
35
+ }: ResourceShowProps): React.ReactElement {
36
+ const config = useAdminConfig();
37
+ const adapter = useDataAdapter();
38
+ const resourceConfig = config.resources.find(r => r.name === resource);
39
+
40
+ const [record, setRecord] = useState<DataRecord | null>(null);
41
+ const [loading, setLoading] = useState(true);
42
+ const [error, setError] = useState<Error | null>(null);
43
+
44
+ const fetchRecord = useCallback(async () => {
45
+ setLoading(true);
46
+ setError(null);
47
+ try {
48
+ const result = await adapter.fetchOne(resource, id);
49
+ setRecord(result);
50
+ } catch (err) {
51
+ setError(err instanceof Error ? err : new Error(String(err)));
52
+ } finally {
53
+ setLoading(false);
54
+ }
55
+ }, [adapter, resource, id]);
56
+
57
+ useEffect(() => {
58
+ fetchRecord();
59
+ }, [fetchRecord]);
60
+
61
+ if (!resourceConfig) {
62
+ return <ErrorState message={`Resource "${resource}" not found in admin configuration`} />;
63
+ }
64
+
65
+ if (loading) {
66
+ return <LoadingState message="Loading record..." />;
67
+ }
68
+
69
+ if (error) {
70
+ return <ErrorState message={error.message} onRetry={fetchRecord} />;
71
+ }
72
+
73
+ if (!record) {
74
+ return <ErrorState message="Record not found" />;
75
+ }
76
+
77
+ const label = resourceConfig.label ?? resourceConfig.name;
78
+ const showFields = resourceConfig.showFields ?? resourceConfig.fields.map(f => f.name);
79
+ const fieldMap = new Map(resourceConfig.fields.map(f => [f.name, f]));
80
+ const canEdit = resourceConfig.actions?.edit !== false;
81
+ const canDelete = resourceConfig.actions?.delete !== false;
82
+
83
+ return (
84
+ <ScrollView className="flex-1">
85
+ {/* Header */}
86
+ <View className="flex-row items-center justify-between px-4 py-4">
87
+ <View className="flex-row items-center">
88
+ {onBackPress && (
89
+ <Button label="\u2190 Back" variant="ghost" onPress={onBackPress} style={{ marginRight: 8 }} />
90
+ )}
91
+ <Text className="text-xl font-bold text-gray-900">
92
+ {label} #{String(id)}
93
+ </Text>
94
+ </View>
95
+ <View className="flex-row gap-2">
96
+ {canEdit && onEditPress && (
97
+ <Button label="Edit" variant="outline" onPress={onEditPress} />
98
+ )}
99
+ {canDelete && onDeletePress && (
100
+ <Button label="Delete" variant="destructive" onPress={onDeletePress} />
101
+ )}
102
+ </View>
103
+ </View>
104
+
105
+ {/* Fields */}
106
+ <Card style={{ marginHorizontal: 16, marginBottom: 16 }}>
107
+ {showFields.map((fieldName, idx) => {
108
+ const fieldDef = fieldMap.get(fieldName);
109
+ if (!fieldDef) return null;
110
+ const value = record[fieldName];
111
+ return (
112
+ <React.Fragment key={fieldName}>
113
+ {idx > 0 && <Divider />}
114
+ <FieldRow field={fieldDef} value={value} />
115
+ </React.Fragment>
116
+ );
117
+ })}
118
+ </Card>
119
+ </ScrollView>
120
+ );
121
+ }
122
+
123
+ // =============================================================================
124
+ // FieldRow
125
+ // =============================================================================
126
+
127
+ interface FieldRowProps {
128
+ field: FieldDefinition;
129
+ value: unknown;
130
+ }
131
+
132
+ function FieldRow({ field, value }: FieldRowProps): React.ReactElement {
133
+ return (
134
+ <View className="py-3">
135
+ <Text className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">
136
+ {field.label ?? field.name}
137
+ </Text>
138
+ {field.type === 'image' ? (
139
+ <ImageDisplay field={field} value={value} />
140
+ ) : (
141
+ <Text className="text-sm text-gray-900">{formatFieldValue(value, field)}</Text>
142
+ )}
143
+ </View>
144
+ );
145
+ }
146
+
147
+ function formatFieldValue(value: unknown, field: FieldDefinition): string {
148
+ if (value == null) return '\u2014';
149
+
150
+ switch (field.type) {
151
+ case 'boolean':
152
+ return value ? 'Yes' : 'No';
153
+ case 'date':
154
+ return new Date(String(value)).toLocaleDateString();
155
+ case 'datetime':
156
+ return new Date(String(value)).toLocaleString();
157
+ case 'json':
158
+ return JSON.stringify(value, null, 2);
159
+ case 'url':
160
+ case 'email':
161
+ case 'image':
162
+ return String(value);
163
+ case 'file': {
164
+ const str = String(value);
165
+ const basename = str.split('/').pop() ?? str;
166
+ return basename;
167
+ }
168
+ default:
169
+ return String(value);
170
+ }
171
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Admin screen components.
3
+ *
4
+ * These components wire admin hooks to UI components to provide
5
+ * ready-made screens for admin dashboards.
6
+ */
7
+
8
+ export { AdminLayout, type AdminLayoutProps } from './AdminLayout';
9
+ export { AdminDashboard, type AdminDashboardProps } from './AdminDashboard';
10
+ export { ResourceList, type ResourceListProps } from './ResourceList';
11
+ export { ResourceShow, type ResourceShowProps } from './ResourceShow';
12
+ export { ResourceCreate, type ResourceCreateProps } from './ResourceCreate';
13
+ export { ResourceEdit, type ResourceEditProps } from './ResourceEdit';
14
+ export { ResourceDashboard, type ResourceDashboardProps } from './ResourceDashboard';