@ramme-io/create-app 1.1.9 → 1.2.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.
- package/package.json +4 -3
- package/template/pkg.json +16 -13
- package/template/src/App.tsx +14 -7
- package/template/src/blocks/SmartTable.tsx +191 -0
- package/template/src/components/AutoForm.tsx +128 -0
- package/template/src/components/DynamicBlock.tsx +37 -31
- package/template/src/components/dev/GhostOverlay.tsx +26 -59
- package/template/src/config/app.manifest.ts +48 -48
- package/template/src/core/component-registry.tsx +21 -41
- package/template/src/core/data-seeder.ts +35 -0
- package/template/src/data/mockData.ts +163 -34
- package/template/src/generated/hooks.ts +34 -55
- package/template/src/hooks/useDataQuery.ts +84 -0
- package/template/src/hooks/useSignal.ts +43 -33
- package/template/src/hooks/useWorkflowEngine.ts +6 -0
- package/template/src/pages/Dashboard.tsx +43 -90
- package/template/src/pages/DynamicPage.tsx +54 -22
- package/template/src/templates/dashboard/DashboardLayout.tsx +2 -0
- package/template/src/templates/dashboard/dashboard.sitemap.ts +23 -73
- package/template/src/types/schema.ts +84 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramme-io/create-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-ramme-app": "./index.js"
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
"build": "cd template && tsc && vite build",
|
|
15
15
|
"preview": "cd template && vite preview",
|
|
16
16
|
"pack-tarball": "npm pack",
|
|
17
|
-
"bundle:ai": "repomix --ignore '**/*.lock,**/dist/**,**/template/dist/**,**/template/node_modules/**' --output 'ramme-starter-context.txt'"
|
|
17
|
+
"bundle:ai": "repomix --ignore '**/*.lock,**/dist/**,**/template/dist/**,**/template/node_modules/**' --output 'ramme-starter-context.txt'",
|
|
18
|
+
"zip:builder": "cd template && zip -r ../base.zip . -x \"node_modules/*\" \"dist/*\" \".DS_Store\" && mv ../base.zip ../../ramme-app-builder/public/base.zip && echo '✅ base.zip updated in Builder!'"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@ramme-io/ui": "^1.1.
|
|
21
|
+
"@ramme-io/ui": "^1.1.5",
|
|
21
22
|
"ag-grid-community": "^34.1.2",
|
|
22
23
|
"ag-grid-enterprise": "^34.1.2",
|
|
23
24
|
"ag-grid-react": "^34.1.2",
|
package/template/pkg.json
CHANGED
|
@@ -9,28 +9,31 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@ramme-io/ui": "^1.1.
|
|
12
|
+
"@ramme-io/ui": "^1.1.5",
|
|
13
13
|
"mqtt": "^5.3.5",
|
|
14
14
|
"zustand": "^4.5.0",
|
|
15
15
|
"ag-grid-community": "^31.3.1",
|
|
16
|
-
"ag-grid-enterprise": "^31.3.1",
|
|
17
|
-
"ag-grid-react": "^31.3.1",
|
|
18
|
-
"framer-motion": "^
|
|
19
|
-
"fs-extra": "^11.
|
|
16
|
+
"ag-grid-enterprise": "^31.3.1",
|
|
17
|
+
"ag-grid-react": "^31.3.1",
|
|
18
|
+
"framer-motion": "^11.0.0",
|
|
19
|
+
"fs-extra": "^11.2.0",
|
|
20
20
|
"react": "^18.2.0",
|
|
21
21
|
"react-dom": "^18.2.0",
|
|
22
|
-
"react-
|
|
23
|
-
"react-
|
|
22
|
+
"react-router-dom": "^6.22.0",
|
|
23
|
+
"react-resizable-panels": "^2.0.9",
|
|
24
|
+
"ai": "^3.0.0",
|
|
25
|
+
"@ai-sdk/google": "^0.0.10",
|
|
26
|
+
"zod": "^3.22.0"
|
|
24
27
|
},
|
|
25
28
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^20.
|
|
27
|
-
"@types/react": "^18.2.
|
|
28
|
-
"@types/react-dom": "^18.2.
|
|
29
|
-
"@vitejs/plugin-react": "^4.2.
|
|
29
|
+
"@types/node": "^20.11.0",
|
|
30
|
+
"@types/react": "^18.2.0",
|
|
31
|
+
"@types/react-dom": "^18.2.0",
|
|
32
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
30
33
|
"autoprefixer": "^10.4.19",
|
|
31
34
|
"postcss": "^8.4.38",
|
|
32
|
-
"tailwindcss": "^3.4.
|
|
33
|
-
"typescript": "^5.2.
|
|
35
|
+
"tailwindcss": "^3.4.3",
|
|
36
|
+
"typescript": "^5.2.0",
|
|
34
37
|
"vite": "^5.2.0"
|
|
35
38
|
}
|
|
36
39
|
}
|
package/template/src/App.tsx
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
2
2
|
import { generateRoutes } from './core/route-generator';
|
|
3
|
+
import { useEffect } from 'react'; // <-- 1. Import useEffect
|
|
3
4
|
|
|
4
5
|
// --- 1. IMPORT ALL THREE TEMPLATES ---
|
|
5
6
|
import DashboardLayout from './templates/dashboard/DashboardLayout';
|
|
6
7
|
import { dashboardSitemap as dashboardSitemap } from './templates/dashboard/dashboard.sitemap';
|
|
7
8
|
import DocsLayout from './templates/docs/DocsLayout';
|
|
8
9
|
import { docsSitemap as docsSitemap } from './templates/docs/docs.sitemap';
|
|
9
|
-
import SettingsLayout from './templates/settings/SettingsLayout';
|
|
10
|
-
import { settingsSitemap as settingsSitemap } from './templates/settings/settings.sitemap';
|
|
10
|
+
import SettingsLayout from './templates/settings/SettingsLayout';
|
|
11
|
+
import { settingsSitemap as settingsSitemap } from './templates/settings/settings.sitemap';
|
|
11
12
|
|
|
12
13
|
// Other Imports
|
|
13
14
|
import ProtectedRoute from './components/ProtectedRoute';
|
|
14
15
|
import LoginPage from './pages/LoginPage';
|
|
15
16
|
import NotFound from './pages/styleguide/NotFound';
|
|
16
17
|
|
|
18
|
+
// --- 2. IMPORT THE SEEDER ---
|
|
19
|
+
import { initializeDataLake } from './core/data-seeder';
|
|
20
|
+
|
|
17
21
|
function App() {
|
|
22
|
+
|
|
23
|
+
// ✅ 3. TRIGGER DATA SEEDING ON MOUNT
|
|
24
|
+
// This checks localStorage and injects our mock database if missing.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
initializeDataLake();
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
18
29
|
return (
|
|
19
30
|
<Routes>
|
|
20
31
|
<Route path="/login" element={<LoginPage />} />
|
|
@@ -24,19 +35,15 @@ function App() {
|
|
|
24
35
|
|
|
25
36
|
{/* Dashboard Template Routes */}
|
|
26
37
|
<Route path="/dashboard/*" element={<DashboardLayout />}>
|
|
27
|
-
{' '}
|
|
28
|
-
{/* <-- Added /* */}
|
|
29
38
|
{generateRoutes(dashboardSitemap)}
|
|
30
39
|
</Route>
|
|
31
40
|
|
|
32
41
|
{/* Docs Template Routes */}
|
|
33
42
|
<Route path="/docs/*" element={<DocsLayout />}>
|
|
34
|
-
{' '}
|
|
35
|
-
{/* <-- Added /* */}
|
|
36
43
|
{generateRoutes(docsSitemap)}
|
|
37
44
|
</Route>
|
|
38
45
|
|
|
39
|
-
{/*
|
|
46
|
+
{/* Settings Layout Route */}
|
|
40
47
|
<Route path="/settings/*" element={<SettingsLayout />}>
|
|
41
48
|
{generateRoutes(settingsSitemap)}
|
|
42
49
|
</Route>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import { DataTable, Button, Icon, Card, useToast, type ColDef } from '@ramme-io/ui';
|
|
3
|
+
import { getResourceMeta, getMockData } from '../data/mockData';
|
|
4
|
+
import { AutoForm } from '../components/AutoForm';
|
|
5
|
+
// ✅ Import types that now definitely exist
|
|
6
|
+
import { useDataQuery, type FilterOption, type SortOption } from '../hooks/useDataQuery';
|
|
7
|
+
import { useCrudLocalStorage } from '../hooks/useCrudLocalStorage';
|
|
8
|
+
|
|
9
|
+
interface SmartTableProps {
|
|
10
|
+
dataId: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
initialFilter?: Record<string, any>;
|
|
13
|
+
initialSort?: SortOption;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SmartTable: React.FC<SmartTableProps> = ({
|
|
17
|
+
dataId,
|
|
18
|
+
title,
|
|
19
|
+
initialFilter,
|
|
20
|
+
initialSort
|
|
21
|
+
}) => {
|
|
22
|
+
const { addToast } = useToast();
|
|
23
|
+
|
|
24
|
+
// 1. STATE
|
|
25
|
+
const [page, setPage] = useState(1);
|
|
26
|
+
const [pageSize] = useState(10); // Kept as state in case we add a selector later
|
|
27
|
+
|
|
28
|
+
// 2. METADATA
|
|
29
|
+
const meta = getResourceMeta(dataId);
|
|
30
|
+
|
|
31
|
+
// 3. STORAGE LAYER
|
|
32
|
+
// Seed data if local storage is empty
|
|
33
|
+
const seedData = useMemo(() => getMockData(dataId) || [], [dataId]);
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
data: rawData,
|
|
37
|
+
createItem,
|
|
38
|
+
updateItem
|
|
39
|
+
} = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
|
|
40
|
+
|
|
41
|
+
// 4. LOGIC LAYER
|
|
42
|
+
const activeFilters = useMemo<FilterOption[]>(() => {
|
|
43
|
+
if (!initialFilter) return [];
|
|
44
|
+
return Object.entries(initialFilter).map(([key, value]) => ({
|
|
45
|
+
field: key,
|
|
46
|
+
operator: 'equals',
|
|
47
|
+
value
|
|
48
|
+
}));
|
|
49
|
+
}, [initialFilter]);
|
|
50
|
+
|
|
51
|
+
const { data: processedRows, total, pageCount } = useDataQuery(rawData, {
|
|
52
|
+
filters: activeFilters,
|
|
53
|
+
sort: initialSort, // Using the prop directly for now (Fixes 'setSort' unused)
|
|
54
|
+
page,
|
|
55
|
+
pageSize
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 5. UI STATE
|
|
59
|
+
const [isEditOpen, setIsEditOpen] = useState(false);
|
|
60
|
+
const [currentRecord, setCurrentRecord] = useState<any>(null);
|
|
61
|
+
|
|
62
|
+
// 6. COLUMNS
|
|
63
|
+
const columns = useMemo(() => {
|
|
64
|
+
if (meta?.fields) {
|
|
65
|
+
const cols: ColDef[] = meta.fields.map((f: any) => {
|
|
66
|
+
const colDef: ColDef = {
|
|
67
|
+
field: f.key,
|
|
68
|
+
headerName: f.label,
|
|
69
|
+
filter: true,
|
|
70
|
+
sortable: true,
|
|
71
|
+
flex: 1,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (f.key.endsWith('Id')) {
|
|
75
|
+
const collectionName = f.key.replace('Id', 's');
|
|
76
|
+
const relatedData = getMockData(collectionName);
|
|
77
|
+
if (relatedData) {
|
|
78
|
+
colDef.valueFormatter = (params) => {
|
|
79
|
+
const match = relatedData.find((item: any) => item.id === params.value);
|
|
80
|
+
return match ? (match.name || match.title || params.value) : params.value;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (f.type === 'currency') colDef.valueFormatter = (p: any) => p.value ? `$${p.value}` : '';
|
|
86
|
+
if (f.type === 'date') colDef.valueFormatter = (p: any) => p.value ? new Date(p.value).toLocaleDateString() : '';
|
|
87
|
+
|
|
88
|
+
if (f.type === 'status') {
|
|
89
|
+
colDef.cellRenderer = (p: any) => (
|
|
90
|
+
<span className={`px-2 py-1 rounded-full text-xs font-medium border
|
|
91
|
+
${['active', 'paid'].includes(p.value?.toLowerCase()) ? 'bg-green-100 text-green-800 border-green-200' :
|
|
92
|
+
['pending'].includes(p.value?.toLowerCase()) ? 'bg-yellow-100 text-yellow-800 border-yellow-200' : 'bg-slate-100 text-slate-800 border-slate-200'}`}>
|
|
93
|
+
{p.value}
|
|
94
|
+
</span>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return colDef;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
cols.push({
|
|
101
|
+
headerName: "Actions",
|
|
102
|
+
field: "id",
|
|
103
|
+
width: 100,
|
|
104
|
+
pinned: 'right',
|
|
105
|
+
cellRenderer: (params: any) => (
|
|
106
|
+
<Button variant="ghost" size="sm" onClick={() => { setCurrentRecord(params.data); setIsEditOpen(true); }}>
|
|
107
|
+
<Icon name="edit-2" className="w-4 h-4 text-slate-500" />
|
|
108
|
+
</Button>
|
|
109
|
+
)
|
|
110
|
+
});
|
|
111
|
+
return cols;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (processedRows.length > 0) {
|
|
115
|
+
return Object.keys(processedRows[0]).map(k => ({ field: k, headerName: k.toUpperCase(), flex: 1 }));
|
|
116
|
+
}
|
|
117
|
+
return [];
|
|
118
|
+
}, [meta, processedRows]);
|
|
119
|
+
|
|
120
|
+
const handleSave = (record: any) => {
|
|
121
|
+
if (record.id && currentRecord?.id) {
|
|
122
|
+
updateItem(record);
|
|
123
|
+
addToast('Record updated successfully', 'success');
|
|
124
|
+
} else {
|
|
125
|
+
const { id, ...newItem } = record;
|
|
126
|
+
createItem(newItem);
|
|
127
|
+
addToast('New record created', 'success');
|
|
128
|
+
}
|
|
129
|
+
setIsEditOpen(false);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Card className="p-0 overflow-hidden border border-border flex flex-col h-full min-h-[500px]">
|
|
134
|
+
<div className="p-4 border-b border-border flex justify-between items-center bg-muted/20">
|
|
135
|
+
<div className="flex items-center gap-2">
|
|
136
|
+
<h3 className="text-lg font-semibold text-foreground">
|
|
137
|
+
{title || meta?.name || dataId}
|
|
138
|
+
</h3>
|
|
139
|
+
<span className="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full">
|
|
140
|
+
{total} records
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
<Button size="sm" variant="outline" onClick={() => { setCurrentRecord({}); setIsEditOpen(true); }}>
|
|
144
|
+
<Icon name="plus" className="mr-2 w-4 h-4" /> Add Item
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="flex-1 w-full bg-card relative">
|
|
149
|
+
<DataTable
|
|
150
|
+
rowData={processedRows}
|
|
151
|
+
columnDefs={columns}
|
|
152
|
+
pagination={false} // We handle pagination logic manually below
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
{/* Pagination Controls */}
|
|
156
|
+
<div className="p-2 border-t border-border flex justify-between items-center text-sm">
|
|
157
|
+
<span className="text-muted-foreground">
|
|
158
|
+
Page {page} of {pageCount || 1}
|
|
159
|
+
</span>
|
|
160
|
+
<div className="flex gap-2">
|
|
161
|
+
<Button
|
|
162
|
+
variant="ghost"
|
|
163
|
+
size="sm"
|
|
164
|
+
disabled={page === 1}
|
|
165
|
+
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
166
|
+
>
|
|
167
|
+
Prev
|
|
168
|
+
</Button>
|
|
169
|
+
<Button
|
|
170
|
+
variant="ghost"
|
|
171
|
+
size="sm"
|
|
172
|
+
disabled={page >= pageCount}
|
|
173
|
+
onClick={() => setPage(p => p + 1)}
|
|
174
|
+
>
|
|
175
|
+
Next
|
|
176
|
+
</Button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<AutoForm
|
|
182
|
+
isOpen={isEditOpen}
|
|
183
|
+
onClose={() => setIsEditOpen(false)}
|
|
184
|
+
onSubmit={handleSave}
|
|
185
|
+
title={meta?.name || 'Item'}
|
|
186
|
+
fields={meta?.fields || []}
|
|
187
|
+
initialData={currentRecord}
|
|
188
|
+
/>
|
|
189
|
+
</Card>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { Drawer, FormTemplate, Button, type FormField } from '@ramme-io/ui';
|
|
3
|
+
|
|
4
|
+
interface AutoFormProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onSubmit: (data: any) => void;
|
|
8
|
+
title: string;
|
|
9
|
+
fields: any[]; // The raw metadata from app.manifest.ts
|
|
10
|
+
initialData?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const AutoForm: React.FC<AutoFormProps> = ({
|
|
14
|
+
isOpen,
|
|
15
|
+
onClose,
|
|
16
|
+
onSubmit,
|
|
17
|
+
title,
|
|
18
|
+
fields,
|
|
19
|
+
initialData
|
|
20
|
+
}) => {
|
|
21
|
+
|
|
22
|
+
// 1. Safe Data Handling
|
|
23
|
+
const safeData = initialData || {};
|
|
24
|
+
const isEditMode = !!safeData.id;
|
|
25
|
+
|
|
26
|
+
// 2. Schema-to-UI Mapping Engine
|
|
27
|
+
const formFields = useMemo<FormField[]>(() => {
|
|
28
|
+
return fields
|
|
29
|
+
// 🛑 RULE 1: Hide ID field during creation (System handles it)
|
|
30
|
+
.filter((f: any) => !(f.key === 'id' && !isEditMode))
|
|
31
|
+
.map((f: any) => {
|
|
32
|
+
|
|
33
|
+
const isIdField = f.key === 'id';
|
|
34
|
+
|
|
35
|
+
// Base Configuration for all fields
|
|
36
|
+
const baseConfig = {
|
|
37
|
+
name: f.key,
|
|
38
|
+
label: f.label,
|
|
39
|
+
required: f.required,
|
|
40
|
+
placeholder: f.description || `Enter ${f.label.toLowerCase()}`,
|
|
41
|
+
// Pre-fill data if editing, or use default from schema
|
|
42
|
+
value: safeData[f.key] !== undefined ? safeData[f.key] : (f.defaultValue || ''),
|
|
43
|
+
colSpan: 1,
|
|
44
|
+
// 🛑 RULE 2: If it's the ID field, force it to be disabled (View Only)
|
|
45
|
+
disabled: isIdField
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Intelligent Type Switching
|
|
49
|
+
switch (f.type) {
|
|
50
|
+
case 'number':
|
|
51
|
+
case 'integer':
|
|
52
|
+
case 'currency':
|
|
53
|
+
return {
|
|
54
|
+
...baseConfig,
|
|
55
|
+
type: 'number',
|
|
56
|
+
value: Number(baseConfig.value) || 0
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
case 'boolean':
|
|
60
|
+
return {
|
|
61
|
+
...baseConfig,
|
|
62
|
+
type: 'toggle',
|
|
63
|
+
checked: !!baseConfig.value
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
case 'date':
|
|
67
|
+
return {
|
|
68
|
+
...baseConfig,
|
|
69
|
+
type: 'datepicker',
|
|
70
|
+
value: baseConfig.value ? new Date(baseConfig.value) : null
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
case 'status':
|
|
74
|
+
return {
|
|
75
|
+
...baseConfig,
|
|
76
|
+
type: 'select',
|
|
77
|
+
options: [
|
|
78
|
+
{ value: 'Active', label: 'Active' },
|
|
79
|
+
{ value: 'Pending', label: 'Pending' },
|
|
80
|
+
{ value: 'Inactive', label: 'Inactive' },
|
|
81
|
+
{ value: 'Archived', label: 'Archived' }
|
|
82
|
+
]
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
case 'textarea':
|
|
86
|
+
return {
|
|
87
|
+
...baseConfig,
|
|
88
|
+
type: 'textarea',
|
|
89
|
+
colSpan: 2
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
case 'email':
|
|
93
|
+
return { ...baseConfig, type: 'email' };
|
|
94
|
+
|
|
95
|
+
default:
|
|
96
|
+
return { ...baseConfig, type: 'text' };
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}, [fields, safeData, isEditMode]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Drawer
|
|
103
|
+
isOpen={isOpen}
|
|
104
|
+
onClose={onClose}
|
|
105
|
+
title={`${isEditMode ? 'Edit' : 'New'} ${title}`}
|
|
106
|
+
size="500px"
|
|
107
|
+
>
|
|
108
|
+
<div className="p-6">
|
|
109
|
+
<FormTemplate
|
|
110
|
+
fields={formFields}
|
|
111
|
+
onSubmit={(formData) => {
|
|
112
|
+
// Merge original ID to ensure we update the correct record
|
|
113
|
+
onSubmit({ ...safeData, ...formData });
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<div className="flex justify-end gap-2 mt-8 pt-4 border-t border-border">
|
|
117
|
+
<Button variant="outline" onClick={onClose} type="button">
|
|
118
|
+
Cancel
|
|
119
|
+
</Button>
|
|
120
|
+
<Button type="submit" variant="primary">
|
|
121
|
+
{isEditMode ? 'Save Changes' : 'Create Record'}
|
|
122
|
+
</Button>
|
|
123
|
+
</div>
|
|
124
|
+
</FormTemplate>
|
|
125
|
+
</div>
|
|
126
|
+
</Drawer>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
@@ -1,52 +1,58 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { getComponent } from '../core/component-registry';
|
|
3
|
-
|
|
4
|
-
import {
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import { useGeneratedSignals } from '../generated/hooks';
|
|
5
|
+
import { getMockData } from '../data/mockData';
|
|
5
6
|
|
|
6
7
|
const mapSignalStatus = (status: string): string => {
|
|
7
8
|
switch (status) {
|
|
8
9
|
case 'fresh': return 'online';
|
|
9
10
|
case 'stale': return 'warning';
|
|
10
11
|
case 'disconnected': return 'offline';
|
|
11
|
-
case 'error': return 'error';
|
|
12
|
+
case 'error': return 'error';
|
|
12
13
|
default: return 'offline';
|
|
13
14
|
}
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
block: {
|
|
18
|
-
id: string;
|
|
19
|
-
type: string;
|
|
20
|
-
props: Record<string, any>;
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const DynamicBlock: React.FC<DynamicBlockProps> = ({ block }) => {
|
|
17
|
+
export const DynamicBlock: React.FC<any> = ({ block }) => {
|
|
25
18
|
const Component = getComponent(block.type);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
// 1. Resolve Data
|
|
29
|
-
const resolvedData = dataId ? getMockData(dataId) : staticProps.data;
|
|
19
|
+
const signals = useGeneratedSignals();
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// 3. Merge Props
|
|
21
|
+
const { signalId, dataId, ...staticProps } = block.props;
|
|
22
|
+
|
|
35
23
|
const dynamicProps: Record<string, any> = {
|
|
36
24
|
...staticProps,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
rowData: resolvedData || [],
|
|
25
|
+
dataId,
|
|
26
|
+
signalId
|
|
40
27
|
};
|
|
41
28
|
|
|
42
|
-
//
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
29
|
+
// --- DATA INJECTION ---
|
|
30
|
+
if (dataId) {
|
|
31
|
+
const resolvedData = getMockData(dataId);
|
|
32
|
+
dynamicProps.data = resolvedData || [];
|
|
33
|
+
dynamicProps.rowData = resolvedData || [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- SIGNAL INJECTION ---
|
|
37
|
+
if (signalId && signals && signalId in signals) {
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
const signalState = signals[signalId];
|
|
40
|
+
|
|
41
|
+
if (signalState) {
|
|
42
|
+
// ✅ FIX: Handle both Raw Values (Legacy) and Signal Objects (New)
|
|
43
|
+
// If it's an object with a 'value' property, use that. Otherwise use it directly.
|
|
44
|
+
const rawValue = (typeof signalState === 'object' && 'value' in signalState)
|
|
45
|
+
? signalState.value
|
|
46
|
+
: signalState;
|
|
47
|
+
|
|
48
|
+
const status = (typeof signalState === 'object' && 'status' in signalState)
|
|
49
|
+
? signalState.status
|
|
50
|
+
: 'fresh'; // Default for raw values
|
|
51
|
+
|
|
52
|
+
dynamicProps.value = typeof rawValue === 'number' ? rawValue : String(rawValue);
|
|
53
|
+
dynamicProps.status = mapSignalStatus(status);
|
|
54
|
+
} else {
|
|
55
|
+
dynamicProps.status = mapSignalStatus('disconnected');
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -1,49 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file GhostOverlay.tsx
|
|
3
|
-
* @repository ramme-app-starter
|
|
4
|
-
* @description
|
|
5
|
-
* A development-mode wrapper that provides "X-Ray vision" into the
|
|
6
|
-
* application's structure.
|
|
7
|
-
*
|
|
8
|
-
* INTENT & PHILOSOPHY:
|
|
9
|
-
* This component embodies the "Glass Box" doctrine of the Ramme Framework.
|
|
10
|
-
* It allows creators to peer behind the "Visual Fidelity" of the prototype
|
|
11
|
-
* and inspect the "Functional Fidelity" underneath.
|
|
12
|
-
*
|
|
13
|
-
* STRATEGIC VALUE:
|
|
14
|
-
* 1. Traceability: It visually links a UI element (e.g., a Gauge) back to
|
|
15
|
-
* its source definition in the manifest (e.g., Signal ID: 'temp_01').
|
|
16
|
-
* 2. Debugging: It helps creators understand layout boundaries (Grid Cells)
|
|
17
|
-
* and data flow without needing browser DevTools.
|
|
18
|
-
* 3. Education: It reinforces the "Architect" mindset by exposing the
|
|
19
|
-
* component hierarchy.
|
|
20
|
-
*
|
|
21
|
-
* USAGE:
|
|
22
|
-
* Wrap any dynamic component in the `Dashboard.tsx` loop with this overlay.
|
|
23
|
-
* It listens to a global `debugMode` state (likely from a store or context)
|
|
24
|
-
* to toggle its visibility.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
1
|
import React from 'react';
|
|
28
|
-
import {
|
|
29
|
-
// We'll need a way to check if we are in "Debug/Ghost" mode.
|
|
30
|
-
// For now, we can pass it as a prop or assume a context hook exists.
|
|
31
|
-
// import { useDevTools } from '../../contexts/DevToolsContext';
|
|
2
|
+
import { Icon } from '@ramme-io/ui';
|
|
32
3
|
|
|
33
4
|
interface GhostOverlayProps {
|
|
34
|
-
/** The actual UI component being wrapped (e.g., DeviceCard) */
|
|
35
5
|
children: React.ReactNode;
|
|
36
|
-
|
|
37
|
-
/** The unique ID of the component from the manifest */
|
|
38
6
|
componentId: string;
|
|
39
|
-
|
|
40
|
-
/** The name of the React component being rendered (e.g., "StatCard") */
|
|
41
7
|
componentType: string;
|
|
42
|
-
|
|
43
|
-
/** (Optional) The ID of the data signal driving this component */
|
|
44
8
|
signalId?: string;
|
|
45
|
-
|
|
46
|
-
/** Whether the overlay is currently active (X-Ray Mode on) */
|
|
47
9
|
isActive?: boolean;
|
|
48
10
|
}
|
|
49
11
|
|
|
@@ -52,48 +14,53 @@ export const GhostOverlay: React.FC<GhostOverlayProps> = ({
|
|
|
52
14
|
componentId,
|
|
53
15
|
componentType,
|
|
54
16
|
signalId,
|
|
55
|
-
isActive = false,
|
|
17
|
+
isActive = false,
|
|
56
18
|
}) => {
|
|
57
19
|
|
|
58
20
|
if (!isActive) {
|
|
59
21
|
return <>{children}</>;
|
|
60
22
|
}
|
|
61
23
|
|
|
24
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
25
|
+
// 🛑 Stop the click from triggering app logic (like navigation or toggles)
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
e.stopPropagation();
|
|
28
|
+
|
|
29
|
+
// 🚀 THE GHOST BRIDGE: Signal the Parent (The Builder)
|
|
30
|
+
window.parent.postMessage({
|
|
31
|
+
type: 'RAMME_SELECT_BLOCK',
|
|
32
|
+
payload: { blockId: componentId }
|
|
33
|
+
}, '*');
|
|
34
|
+
};
|
|
35
|
+
|
|
62
36
|
return (
|
|
63
|
-
<div
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*/}
|
|
68
|
-
<div className="absolute inset-0 z-50
|
|
37
|
+
<div
|
|
38
|
+
className="relative group cursor-pointer"
|
|
39
|
+
onClick={handleClick} // ✅ Add Click Handler
|
|
40
|
+
>
|
|
41
|
+
{/* The "Ghost" Border */}
|
|
42
|
+
<div className="absolute inset-0 z-50 border-2 border-dashed border-accent/50 rounded-lg bg-accent/5 group-hover:bg-accent/10 transition-colors pointer-events-none" />
|
|
69
43
|
|
|
70
|
-
{/* The "Info Tag"
|
|
71
|
-
|
|
72
|
-
- Displays the technical 'bones' of the component
|
|
73
|
-
*/}
|
|
74
|
-
<div className="absolute top-0 left-0 z-50 p-2 transform -translate-y-1/2 translate-x-2">
|
|
44
|
+
{/* The "Info Tag" */}
|
|
45
|
+
<div className="absolute top-0 left-0 z-50 p-2 transform -translate-y-1/2 translate-x-2 pointer-events-none">
|
|
75
46
|
<div className="flex items-center gap-2 bg-accent text-accent-foreground text-xs font-mono py-1 px-2 rounded shadow-sm">
|
|
76
47
|
<Icon name="box" size={12} />
|
|
77
48
|
<span className="font-bold">{componentType}</span>
|
|
78
|
-
<span className="opacity-75">#{componentId}</span>
|
|
79
49
|
</div>
|
|
80
50
|
</div>
|
|
81
51
|
|
|
82
|
-
{/* The "Signal Wire"
|
|
83
|
-
- Floats bottom-right if a signal is connected
|
|
84
|
-
- Shows the data source wiring
|
|
85
|
-
*/}
|
|
52
|
+
{/* The "Signal Wire" */}
|
|
86
53
|
{signalId && (
|
|
87
|
-
<div className="absolute bottom-0 right-0 z-50 p-2 transform translate-y-1/2 -translate-x-2">
|
|
54
|
+
<div className="absolute bottom-0 right-0 z-50 p-2 transform translate-y-1/2 -translate-x-2 pointer-events-none">
|
|
88
55
|
<div className="flex items-center gap-1.5 bg-blue-600 text-white text-xs font-mono py-1 px-2 rounded shadow-sm">
|
|
89
56
|
<Icon name="activity" size={12} />
|
|
90
|
-
<span>
|
|
57
|
+
<span>{signalId}</span>
|
|
91
58
|
</div>
|
|
92
59
|
</div>
|
|
93
60
|
)}
|
|
94
61
|
|
|
95
62
|
{/* Render the actual component underneath */}
|
|
96
|
-
<div className="opacity-50 grayscale transition-all duration-200 group-hover:opacity-75 group-hover:grayscale-0">
|
|
63
|
+
<div className="opacity-50 grayscale transition-all duration-200 group-hover:opacity-75 group-hover:grayscale-0 pointer-events-none">
|
|
97
64
|
{children}
|
|
98
65
|
</div>
|
|
99
66
|
</div>
|