@ramme-io/kernel 1.3.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/.turbo/turbo-build.log +5 -0
- package/LICENSE +21 -0
- package/dist/components/AutoForm.d.ts +24 -0
- package/dist/components/AutoForm.d.ts.map +1 -0
- package/dist/components/AutoForm.js +78 -0
- package/dist/components/AutoForm.js.map +1 -0
- package/dist/components/SmartTable.d.ts +14 -0
- package/dist/components/SmartTable.d.ts.map +1 -0
- package/dist/components/SmartTable.js +128 -0
- package/dist/components/SmartTable.js.map +1 -0
- package/dist/config/app.manifest.d.ts +7 -0
- package/dist/config/app.manifest.d.ts.map +1 -0
- package/dist/config/app.manifest.js +8 -0
- package/dist/config/app.manifest.js.map +1 -0
- package/dist/engine/renderers/DynamicBlock.d.ts +9 -0
- package/dist/engine/renderers/DynamicBlock.d.ts.map +1 -0
- package/dist/engine/renderers/DynamicBlock.js +67 -0
- package/dist/engine/renderers/DynamicBlock.js.map +1 -0
- package/dist/engine/renderers/DynamicPage.d.ts +11 -0
- package/dist/engine/renderers/DynamicPage.d.ts.map +1 -0
- package/dist/engine/renderers/DynamicPage.js +91 -0
- package/dist/engine/renderers/DynamicPage.js.map +1 -0
- package/dist/engine/renderers/route-generator.d.ts +16 -0
- package/dist/engine/renderers/route-generator.d.ts.map +1 -0
- package/dist/engine/renderers/route-generator.js +30 -0
- package/dist/engine/renderers/route-generator.js.map +1 -0
- package/dist/engine/renderers/sitemap-entry.d.ts +7 -0
- package/dist/engine/renderers/sitemap-entry.d.ts.map +1 -0
- package/dist/engine/renderers/sitemap-entry.js +2 -0
- package/dist/engine/renderers/sitemap-entry.js.map +1 -0
- package/dist/engine/runtime/ManifestContext.d.ts +115 -0
- package/dist/engine/runtime/ManifestContext.d.ts.map +1 -0
- package/dist/engine/runtime/ManifestContext.js +56 -0
- package/dist/engine/runtime/ManifestContext.js.map +1 -0
- package/dist/engine/runtime/MqttContext.d.ts +14 -0
- package/dist/engine/runtime/MqttContext.d.ts.map +1 -0
- package/dist/engine/runtime/MqttContext.js +70 -0
- package/dist/engine/runtime/MqttContext.js.map +1 -0
- package/dist/engine/runtime/SitemapContext.d.ts +31 -0
- package/dist/engine/runtime/SitemapContext.d.ts.map +1 -0
- package/dist/engine/runtime/SitemapContext.js +54 -0
- package/dist/engine/runtime/SitemapContext.js.map +1 -0
- package/dist/engine/runtime/data-seeder.d.ts +10 -0
- package/dist/engine/runtime/data-seeder.d.ts.map +1 -0
- package/dist/engine/runtime/data-seeder.js +35 -0
- package/dist/engine/runtime/data-seeder.js.map +1 -0
- package/dist/engine/runtime/useAction.d.ts +4 -0
- package/dist/engine/runtime/useAction.d.ts.map +1 -0
- package/dist/engine/runtime/useAction.js +55 -0
- package/dist/engine/runtime/useAction.js.map +1 -0
- package/dist/engine/runtime/useCrudLocalStorage.d.ts +19 -0
- package/dist/engine/runtime/useCrudLocalStorage.d.ts.map +1 -0
- package/dist/engine/runtime/useCrudLocalStorage.js +73 -0
- package/dist/engine/runtime/useCrudLocalStorage.js.map +1 -0
- package/dist/engine/runtime/useDataQuery.d.ts +39 -0
- package/dist/engine/runtime/useDataQuery.d.ts.map +1 -0
- package/dist/engine/runtime/useDataQuery.js +50 -0
- package/dist/engine/runtime/useDataQuery.js.map +1 -0
- package/dist/engine/runtime/useDynamicSitemap.d.ts +9 -0
- package/dist/engine/runtime/useDynamicSitemap.d.ts.map +1 -0
- package/dist/engine/runtime/useDynamicSitemap.js +38 -0
- package/dist/engine/runtime/useDynamicSitemap.js.map +1 -0
- package/dist/engine/runtime/useJustInTimeSeeder.d.ts +11 -0
- package/dist/engine/runtime/useJustInTimeSeeder.d.ts.map +1 -0
- package/dist/engine/runtime/useJustInTimeSeeder.js +88 -0
- package/dist/engine/runtime/useJustInTimeSeeder.js.map +1 -0
- package/dist/engine/runtime/useLiveBridge.d.ts +109 -0
- package/dist/engine/runtime/useLiveBridge.d.ts.map +1 -0
- package/dist/engine/runtime/useLiveBridge.js +21 -0
- package/dist/engine/runtime/useLiveBridge.js.map +1 -0
- package/dist/engine/runtime/useSignal.d.ts +11 -0
- package/dist/engine/runtime/useSignal.d.ts.map +1 -0
- package/dist/engine/runtime/useSignal.js +26 -0
- package/dist/engine/runtime/useSignal.js.map +1 -0
- package/dist/engine/runtime/useSignalStore.d.ts +31 -0
- package/dist/engine/runtime/useSignalStore.d.ts.map +1 -0
- package/dist/engine/runtime/useSignalStore.js +60 -0
- package/dist/engine/runtime/useSignalStore.js.map +1 -0
- package/dist/engine/runtime/useWorkflowEngine.d.ts +4 -0
- package/dist/engine/runtime/useWorkflowEngine.d.ts.map +1 -0
- package/dist/engine/runtime/useWorkflowEngine.js +85 -0
- package/dist/engine/runtime/useWorkflowEngine.js.map +1 -0
- package/dist/engine/types/manifest-types.d.ts +38 -0
- package/dist/engine/types/manifest-types.d.ts.map +1 -0
- package/dist/engine/types/manifest-types.js +5 -0
- package/dist/engine/types/manifest-types.js.map +1 -0
- package/dist/engine/types/sitemap-entry.d.ts +58 -0
- package/dist/engine/types/sitemap-entry.d.ts.map +1 -0
- package/dist/engine/types/sitemap-entry.js +19 -0
- package/dist/engine/types/sitemap-entry.js.map +1 -0
- package/dist/engine/validation/schema.d.ts +383 -0
- package/dist/engine/validation/schema.d.ts.map +1 -0
- package/dist/engine/validation/schema.js +156 -0
- package/dist/engine/validation/schema.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/components/AutoForm.tsx +141 -0
- package/src/components/SmartTable.tsx +316 -0
- package/src/config/app.manifest.ts +7 -0
- package/src/engine/renderers/DynamicBlock.tsx +84 -0
- package/src/engine/renderers/DynamicPage.tsx +196 -0
- package/src/engine/renderers/route-generator.tsx +47 -0
- package/src/engine/renderers/sitemap-entry.ts +6 -0
- package/src/engine/runtime/ManifestContext.tsx +81 -0
- package/src/engine/runtime/MqttContext.tsx +94 -0
- package/src/engine/runtime/SitemapContext.tsx +61 -0
- package/src/engine/runtime/data-seeder.ts +39 -0
- package/src/engine/runtime/useAction.ts +64 -0
- package/src/engine/runtime/useCrudLocalStorage.ts +82 -0
- package/src/engine/runtime/useDataQuery.ts +98 -0
- package/src/engine/runtime/useDynamicSitemap.tsx +43 -0
- package/src/engine/runtime/useJustInTimeSeeder.ts +101 -0
- package/src/engine/runtime/useLiveBridge.ts +24 -0
- package/src/engine/runtime/useSignal.ts +40 -0
- package/src/engine/runtime/useSignalStore.ts +94 -0
- package/src/engine/runtime/useWorkflowEngine.ts +89 -0
- package/src/engine/types/manifest-types.ts +45 -0
- package/src/engine/types/sitemap-entry.ts +66 -0
- package/src/engine/validation/schema.ts +189 -0
- package/src/index.ts +27 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
DataTable,
|
|
4
|
+
Button,
|
|
5
|
+
Icon,
|
|
6
|
+
Card,
|
|
7
|
+
Badge,
|
|
8
|
+
useToast,
|
|
9
|
+
SearchInput,
|
|
10
|
+
type ColDef,
|
|
11
|
+
type GridApi
|
|
12
|
+
} from '@ramme-io/ui';
|
|
13
|
+
import { useJustInTimeSeeder } from '../engine/runtime/useJustInTimeSeeder';
|
|
14
|
+
import { useCrudLocalStorage } from '../engine/runtime/useCrudLocalStorage';
|
|
15
|
+
import { useManifest } from '../engine/runtime/ManifestContext';
|
|
16
|
+
import { AutoForm } from './AutoForm';
|
|
17
|
+
// ✅ IMPORT FieldDefinition specifically
|
|
18
|
+
import type { ResourceDefinition, FieldDefinition } from '../engine/validation/schema';
|
|
19
|
+
|
|
20
|
+
interface SmartTableProps {
|
|
21
|
+
dataId: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
initialFilter?: Record<string, any>;
|
|
24
|
+
// Optional: Inject external dependency for metadata resolution
|
|
25
|
+
getResourceMeta?: (id: string) => { name: string; fields: FieldDefinition[] } | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SmartTable: React.FC<SmartTableProps> = ({
|
|
29
|
+
dataId,
|
|
30
|
+
title,
|
|
31
|
+
getResourceMeta
|
|
32
|
+
}) => {
|
|
33
|
+
const { addToast } = useToast();
|
|
34
|
+
const manifest = useManifest();
|
|
35
|
+
|
|
36
|
+
// --- 1. METADATA RESOLUTION ---
|
|
37
|
+
const meta = useMemo<ResourceDefinition | null>(() => {
|
|
38
|
+
const dynamicResource = manifest.resources?.find((r: ResourceDefinition) => r.id === dataId);
|
|
39
|
+
if (dynamicResource) return dynamicResource;
|
|
40
|
+
|
|
41
|
+
const staticMeta = getResourceMeta?.(dataId);
|
|
42
|
+
if (staticMeta) {
|
|
43
|
+
return {
|
|
44
|
+
...staticMeta,
|
|
45
|
+
id: dataId,
|
|
46
|
+
} as unknown as ResourceDefinition;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}, [manifest, dataId]);
|
|
50
|
+
|
|
51
|
+
// --- 2. DATA HYDRATION ---
|
|
52
|
+
const seedData = useJustInTimeSeeder(dataId, meta);
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
data: rowData,
|
|
56
|
+
createItem,
|
|
57
|
+
updateItem,
|
|
58
|
+
deleteItem
|
|
59
|
+
} = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
|
|
60
|
+
|
|
61
|
+
// --- 3. UI STATE ---
|
|
62
|
+
const [isEditOpen, setIsEditOpen] = useState(false);
|
|
63
|
+
const [currentRecord, setCurrentRecord] = useState<any>(null);
|
|
64
|
+
const [gridApi, setGridApi] = useState<GridApi | null>(null);
|
|
65
|
+
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
66
|
+
const [quickFilterText, setQuickFilterText] = useState('');
|
|
67
|
+
|
|
68
|
+
// --- 4. COLUMN DEFINITIONS (Desktop) ---
|
|
69
|
+
const columns = useMemo<ColDef[]>(() => {
|
|
70
|
+
if (!meta?.fields) return [];
|
|
71
|
+
|
|
72
|
+
const generatedCols: ColDef[] = meta.fields.map((f: FieldDefinition) => {
|
|
73
|
+
const col: ColDef = {
|
|
74
|
+
field: f.key,
|
|
75
|
+
headerName: f.label,
|
|
76
|
+
filter: true,
|
|
77
|
+
sortable: true,
|
|
78
|
+
resizable: true,
|
|
79
|
+
flex: 1,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (f.type === 'currency') {
|
|
83
|
+
col.valueFormatter = (p: any) => p.value ? `$${Number(p.value).toLocaleString()}` : '';
|
|
84
|
+
}
|
|
85
|
+
if (f.type === 'date') {
|
|
86
|
+
col.valueFormatter = (p: any) => p.value ? new Date(p.value).toLocaleDateString() : '';
|
|
87
|
+
}
|
|
88
|
+
if (f.type === 'status') {
|
|
89
|
+
col.cellRenderer = (p: any) => {
|
|
90
|
+
const statusColors: any = {
|
|
91
|
+
active: 'bg-green-100 text-green-800',
|
|
92
|
+
paid: 'bg-green-100 text-green-800',
|
|
93
|
+
pending: 'bg-yellow-100 text-yellow-800',
|
|
94
|
+
inactive: 'bg-slate-100 text-slate-600',
|
|
95
|
+
overdue: 'bg-red-100 text-red-800'
|
|
96
|
+
};
|
|
97
|
+
const colorClass = statusColors[String(p.value).toLowerCase()] || 'bg-slate-100 text-slate-800';
|
|
98
|
+
return (
|
|
99
|
+
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
|
|
100
|
+
{p.value}
|
|
101
|
+
</span>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return col;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (generatedCols.length > 0) {
|
|
109
|
+
generatedCols[0].headerCheckboxSelection = true;
|
|
110
|
+
generatedCols[0].checkboxSelection = true;
|
|
111
|
+
generatedCols[0].minWidth = 180;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
generatedCols.push({
|
|
115
|
+
headerName: "Actions",
|
|
116
|
+
field: "id",
|
|
117
|
+
width: 100,
|
|
118
|
+
pinned: 'right',
|
|
119
|
+
cellRenderer: (params: any) => (
|
|
120
|
+
<div className="flex items-center gap-1">
|
|
121
|
+
{/* ✅ FIXED: Explicit React.MouseEvent type */}
|
|
122
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={(e: React.MouseEvent) => { e.stopPropagation(); setCurrentRecord(params.data); setIsEditOpen(true); }}>
|
|
123
|
+
<Icon name="edit-2" size={14} className="text-slate-500" />
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return generatedCols;
|
|
130
|
+
}, [meta]);
|
|
131
|
+
|
|
132
|
+
// --- 5. FIELD HELPERS (Mobile) ---
|
|
133
|
+
// ✅ FIXED: Explicit FieldDefinition type for 'f'
|
|
134
|
+
const titleField = useMemo(() => meta?.fields.find((f: FieldDefinition) => f.type === 'text' && f.key !== 'id') || meta?.fields[0], [meta]);
|
|
135
|
+
const statusField = useMemo(() => meta?.fields.find((f: FieldDefinition) => f.type === 'status'), [meta]);
|
|
136
|
+
|
|
137
|
+
// --- 6. HANDLERS ---
|
|
138
|
+
const onGridReady = useCallback((params: any) => {
|
|
139
|
+
setGridApi(params.api);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const onSelectionChanged = useCallback(() => {
|
|
143
|
+
if (gridApi) {
|
|
144
|
+
setSelectedRows(gridApi.getSelectedRows());
|
|
145
|
+
}
|
|
146
|
+
}, [gridApi]);
|
|
147
|
+
|
|
148
|
+
const handleBulkDelete = () => {
|
|
149
|
+
if (confirm(`Delete ${selectedRows.length} items?`)) {
|
|
150
|
+
selectedRows.forEach(row => deleteItem(row.id));
|
|
151
|
+
setSelectedRows([]);
|
|
152
|
+
addToast(`${selectedRows.length} items deleted`, 'success');
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handleSave = (record: any) => {
|
|
157
|
+
if (record.id && currentRecord?.id) {
|
|
158
|
+
updateItem(record);
|
|
159
|
+
addToast('Item updated', 'success');
|
|
160
|
+
} else {
|
|
161
|
+
const { id, ...newItem } = record;
|
|
162
|
+
createItem(newItem);
|
|
163
|
+
addToast('Item created', 'success');
|
|
164
|
+
}
|
|
165
|
+
setIsEditOpen(false);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Card className="flex flex-col h-[600px] border border-border shadow-sm overflow-hidden bg-card">
|
|
170
|
+
|
|
171
|
+
{/* --- HEADER --- */}
|
|
172
|
+
<div className="p-4 border-b border-border flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-muted/5">
|
|
173
|
+
{selectedRows.length > 0 ? (
|
|
174
|
+
<div className="flex items-center gap-3 animate-in fade-in slide-in-from-left-2 duration-200">
|
|
175
|
+
<span className="bg-primary text-primary-foreground text-xs font-bold px-2 py-1 rounded-md">
|
|
176
|
+
{selectedRows.length} Selected
|
|
177
|
+
</span>
|
|
178
|
+
<Button size="sm" variant="danger" onClick={handleBulkDelete} iconLeft="trash-2">
|
|
179
|
+
Delete
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
) : (
|
|
183
|
+
<div className="flex items-center gap-2">
|
|
184
|
+
<div className="p-2 bg-primary/10 rounded-md text-primary">
|
|
185
|
+
<Icon name="table" size={18} />
|
|
186
|
+
</div>
|
|
187
|
+
<div>
|
|
188
|
+
<h3 className="text-base font-bold text-foreground leading-tight">
|
|
189
|
+
{title || meta?.name || dataId}
|
|
190
|
+
</h3>
|
|
191
|
+
<p className="text-xs text-muted-foreground">
|
|
192
|
+
{rowData.length} records found
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
199
|
+
<div className="w-full sm:w-64">
|
|
200
|
+
<SearchInput
|
|
201
|
+
placeholder="Quick search..."
|
|
202
|
+
value={quickFilterText}
|
|
203
|
+
// ✅ FIXED: Explicit ChangeEvent type
|
|
204
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
205
|
+
setQuickFilterText(e.target.value);
|
|
206
|
+
gridApi?.updateGridOptions({ quickFilterText: e.target.value });
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
<div className="h-6 w-px bg-border mx-1 hidden sm:block" />
|
|
211
|
+
<Button size="sm" variant="primary" iconLeft="plus" onClick={() => { setCurrentRecord({}); setIsEditOpen(true); }}>
|
|
212
|
+
Add
|
|
213
|
+
</Button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* --- CONTENT AREA --- */}
|
|
218
|
+
<div className="flex-1 w-full bg-card relative overflow-hidden">
|
|
219
|
+
|
|
220
|
+
{/* 🖥️ DESKTOP VIEW: The Power Grid */}
|
|
221
|
+
<div className="hidden md:block h-full">
|
|
222
|
+
<DataTable
|
|
223
|
+
rowData={rowData}
|
|
224
|
+
columnDefs={columns}
|
|
225
|
+
onGridReady={onGridReady}
|
|
226
|
+
onSelectionChanged={onSelectionChanged}
|
|
227
|
+
rowSelection="multiple"
|
|
228
|
+
pagination={true}
|
|
229
|
+
paginationPageSize={10}
|
|
230
|
+
headerHeight={48}
|
|
231
|
+
rowHeight={48}
|
|
232
|
+
enableCellTextSelection={true}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* 📱 MOBILE VIEW: The Card List */}
|
|
237
|
+
<div className="block md:hidden h-full overflow-y-auto p-4 space-y-3 bg-muted/5">
|
|
238
|
+
{rowData.map((row) => (
|
|
239
|
+
<div key={row.id} className="bg-background border border-border rounded-lg p-4 shadow-sm relative group">
|
|
240
|
+
|
|
241
|
+
{/* Header: Title + Status */}
|
|
242
|
+
<div className="flex justify-between items-start mb-2">
|
|
243
|
+
<div>
|
|
244
|
+
<h4 className="font-semibold text-foreground">
|
|
245
|
+
{titleField ? row[titleField.key] : row.id}
|
|
246
|
+
</h4>
|
|
247
|
+
<p className="text-xs text-muted-foreground font-mono mt-0.5 opacity-70">{row.id}</p>
|
|
248
|
+
</div>
|
|
249
|
+
{statusField && (
|
|
250
|
+
<Badge variant={
|
|
251
|
+
['active', 'paid'].includes(String(row[statusField.key]).toLowerCase()) ? 'success' :
|
|
252
|
+
['pending'].includes(String(row[statusField.key]).toLowerCase()) ? 'warning' : 'secondary'
|
|
253
|
+
}>
|
|
254
|
+
{row[statusField.key]}
|
|
255
|
+
</Badge>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Body: Detailed Fields */}
|
|
260
|
+
<div className="space-y-1 text-sm text-muted-foreground mb-4">
|
|
261
|
+
{meta?.fields
|
|
262
|
+
// ✅ FIXED: Explicit FieldDefinition type for 'f'
|
|
263
|
+
.filter((f: FieldDefinition) => f.key !== titleField?.key && f.key !== statusField?.key && f.key !== 'id')
|
|
264
|
+
.slice(0, 3)
|
|
265
|
+
.map((f: FieldDefinition) => (
|
|
266
|
+
<div key={f.key} className="flex justify-between border-b border-dashed border-border/50 pb-1 last:border-0">
|
|
267
|
+
<span className="opacity-70">{f.label}:</span>
|
|
268
|
+
<span className="font-medium text-foreground">
|
|
269
|
+
{f.type === 'currency' ? `$${Number(row[f.key]).toLocaleString()}` :
|
|
270
|
+
f.type === 'date' ? new Date(row[f.key]).toLocaleDateString() : row[f.key]}
|
|
271
|
+
</span>
|
|
272
|
+
</div>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{/* Footer: Actions */}
|
|
277
|
+
<div className="flex gap-2 pt-2 border-t border-border/50">
|
|
278
|
+
<Button
|
|
279
|
+
size="sm"
|
|
280
|
+
variant="outline"
|
|
281
|
+
className="flex-1 h-8 text-xs"
|
|
282
|
+
onClick={() => { setCurrentRecord(row); setIsEditOpen(true); }}
|
|
283
|
+
>
|
|
284
|
+
<Icon name="edit-2" size={12} className="mr-2"/> Edit
|
|
285
|
+
</Button>
|
|
286
|
+
<Button
|
|
287
|
+
size="sm"
|
|
288
|
+
variant="ghost"
|
|
289
|
+
className="text-destructive hover:bg-destructive/10 px-3 h-8"
|
|
290
|
+
onClick={() => { if(confirm('Delete?')) deleteItem(row.id); }}
|
|
291
|
+
>
|
|
292
|
+
<Icon name="trash-2" size={14} />
|
|
293
|
+
</Button>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
</div>
|
|
297
|
+
))}
|
|
298
|
+
|
|
299
|
+
{rowData.length === 0 && (
|
|
300
|
+
<div className="text-center p-8 text-muted-foreground">No records found.</div>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<AutoForm
|
|
307
|
+
isOpen={isEditOpen}
|
|
308
|
+
onClose={() => setIsEditOpen(false)}
|
|
309
|
+
onSubmit={handleSave}
|
|
310
|
+
title={meta?.name || 'Item'}
|
|
311
|
+
fields={meta?.fields || []}
|
|
312
|
+
initialData={currentRecord}
|
|
313
|
+
/>
|
|
314
|
+
</Card>
|
|
315
|
+
);
|
|
316
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import { useGeneratedSignals } from '../runtime/useSignalStore';
|
|
4
|
+
|
|
5
|
+
interface DynamicBlockProps {
|
|
6
|
+
block: any;
|
|
7
|
+
getComponent: (name: string) => React.FC<any>;
|
|
8
|
+
getMockData: (id: string) => any[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @file DynamicBlock.tsx
|
|
13
|
+
* @description The "Runtime Hydrator" for the application.
|
|
14
|
+
*
|
|
15
|
+
* ARCHITECTURAL ROLE:
|
|
16
|
+
* This component acts as the bridge between the Abstract Syntax Tree (JSON Manifest)
|
|
17
|
+
* and the concrete React UI.
|
|
18
|
+
*
|
|
19
|
+
* KEY RESPONSIBILITIES:
|
|
20
|
+
* 1. **Component Lookup:** Resolves string types ('StatCard') to actual React components.
|
|
21
|
+
* 2. **Signal Injection:** Subscribes to the real-time Signal Engine and feeds live values to props.
|
|
22
|
+
* 3. **Data Hydration:** Fetches static or async data (users, logs) based on `dataId`.
|
|
23
|
+
* 4. **Status Normalization:** Translates system-level signal states into UI-friendly status colors.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const mapSignalStatus = (status: string): string => {
|
|
27
|
+
switch (status) {
|
|
28
|
+
case 'fresh': return 'online';
|
|
29
|
+
case 'stale': return 'warning';
|
|
30
|
+
case 'disconnected': return 'offline';
|
|
31
|
+
case 'error': return 'error';
|
|
32
|
+
default: return 'offline';
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const DynamicBlock: React.FC<DynamicBlockProps> = ({ block, getComponent, getMockData }) => {
|
|
37
|
+
const Component = getComponent(block.type);
|
|
38
|
+
const signals = useGeneratedSignals();
|
|
39
|
+
|
|
40
|
+
const { signalId, dataId, ...staticProps } = block.props;
|
|
41
|
+
|
|
42
|
+
const dynamicProps: Record<string, any> = {
|
|
43
|
+
...staticProps,
|
|
44
|
+
dataId,
|
|
45
|
+
signalId
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// --- DATA INJECTION ---
|
|
49
|
+
if (dataId) {
|
|
50
|
+
const resolvedData = getMockData(dataId);
|
|
51
|
+
dynamicProps.data = resolvedData || [];
|
|
52
|
+
dynamicProps.rowData = resolvedData || [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- SIGNAL INJECTION ---
|
|
56
|
+
if (signalId && signals && signalId in signals) {
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
const signalState = signals[signalId];
|
|
59
|
+
|
|
60
|
+
if (signalState) {
|
|
61
|
+
// ✅ FIX: Check for null and handle objects vs raw values
|
|
62
|
+
const isSignalObject = typeof signalState === 'object' && signalState !== null;
|
|
63
|
+
|
|
64
|
+
// Extract Value
|
|
65
|
+
const rawValue = (isSignalObject && 'value' in signalState)
|
|
66
|
+
? signalState.value
|
|
67
|
+
: signalState;
|
|
68
|
+
|
|
69
|
+
// Extract Status
|
|
70
|
+
const rawStatus = (isSignalObject && 'status' in signalState)
|
|
71
|
+
? signalState.status
|
|
72
|
+
: 'fresh'; // Default for raw values
|
|
73
|
+
|
|
74
|
+
dynamicProps.value = typeof rawValue === 'number' ? rawValue : String(rawValue);
|
|
75
|
+
|
|
76
|
+
// ✅ FIX: Explicitly cast to String to satisfy TypeScript
|
|
77
|
+
dynamicProps.status = mapSignalStatus(String(rawStatus));
|
|
78
|
+
} else {
|
|
79
|
+
dynamicProps.status = mapSignalStatus('disconnected');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return <Component key={block.id} {...dynamicProps} />;
|
|
84
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/kernel/src/engine/renderers/DynamicPage.tsx
|
|
3
|
+
* @description The main page renderer for the dashboard.
|
|
4
|
+
* * ARCHITECTURAL ROLE:
|
|
5
|
+
* This component is the "Engine Room" of the dashboard.
|
|
6
|
+
* It orchestrates the layout and delegates rendering to DynamicBlock.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useMemo, type ReactNode } from 'react';
|
|
10
|
+
import {
|
|
11
|
+
Badge,
|
|
12
|
+
StatCard,
|
|
13
|
+
DeviceCard,
|
|
14
|
+
BarChart,
|
|
15
|
+
LineChart,
|
|
16
|
+
Button
|
|
17
|
+
} from '@ramme-io/ui';
|
|
18
|
+
|
|
19
|
+
// ✅ INTERNAL: Import your SmartTable (Logic Component)
|
|
20
|
+
import { SmartTable } from '../../components/SmartTable';
|
|
21
|
+
|
|
22
|
+
// ❌ DELETED: No more static data dependency
|
|
23
|
+
// import { DATA_REGISTRY } from '../../data/mockData';
|
|
24
|
+
|
|
25
|
+
// ✅ NEW: Import the unified Data Hook
|
|
26
|
+
import { useJustInTimeSeeder } from '../runtime/useJustInTimeSeeder';
|
|
27
|
+
|
|
28
|
+
import { useManifest, useBridgeStatus } from '../runtime/ManifestContext';
|
|
29
|
+
import { Wifi, WifiOff, AlertTriangle, Loader2, Database, Wand2, LayoutTemplate } from 'lucide-react';
|
|
30
|
+
|
|
31
|
+
// ------------------------------------------------------------------
|
|
32
|
+
// 1. THE SAFETY REGISTRY
|
|
33
|
+
// ------------------------------------------------------------------
|
|
34
|
+
const COMPONENT_REGISTRY: Record<string, React.FC<any>> = {
|
|
35
|
+
'SmartTable': SmartTable,
|
|
36
|
+
'DataTable': SmartTable,
|
|
37
|
+
'StatCard': StatCard,
|
|
38
|
+
'DeviceCard': DeviceCard,
|
|
39
|
+
'BarChart': BarChart,
|
|
40
|
+
'LineChart': LineChart,
|
|
41
|
+
'Button': Button,
|
|
42
|
+
'Unknown': ({ type }: { type: string }) => (
|
|
43
|
+
<div className="h-24 w-full border-2 border-dashed border-amber-300 bg-amber-50 rounded-lg flex flex-col items-center justify-center text-amber-700 text-sm">
|
|
44
|
+
<LayoutTemplate className="mb-2 opacity-50" size={20} />
|
|
45
|
+
<span>Unknown Component: <strong>{type}</strong></span>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getComponent = (name: string) => {
|
|
51
|
+
return COMPONENT_REGISTRY[name] || COMPONENT_REGISTRY['Unknown'];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ------------------------------------------------------------------
|
|
55
|
+
// 2. ERROR BOUNDARY
|
|
56
|
+
// ------------------------------------------------------------------
|
|
57
|
+
interface ErrorBoundaryProps { children: ReactNode; }
|
|
58
|
+
interface ErrorBoundaryState { hasError: boolean; error: string; }
|
|
59
|
+
|
|
60
|
+
class BlockErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
61
|
+
constructor(props: ErrorBoundaryProps) {
|
|
62
|
+
super(props);
|
|
63
|
+
this.state = { hasError: false, error: '' };
|
|
64
|
+
}
|
|
65
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
66
|
+
return { hasError: true, error: error.message };
|
|
67
|
+
}
|
|
68
|
+
render() {
|
|
69
|
+
if (this.state.hasError) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="h-full min-h-[100px] p-4 border-2 border-dashed border-red-300 bg-red-50/50 rounded-lg flex flex-col items-center justify-center text-red-600 text-xs">
|
|
72
|
+
<AlertTriangle size={16} className="mb-2 opacity-80" />
|
|
73
|
+
<span className="font-bold">Render Error</span>
|
|
74
|
+
<span className="opacity-75 text-center truncate max-w-[200px]">{this.state.error}</span>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return this.props.children;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ------------------------------------------------------------------
|
|
83
|
+
// 3. DYNAMIC BLOCK (New Sub-Component)
|
|
84
|
+
// ------------------------------------------------------------------
|
|
85
|
+
// We extracted this so we can use hooks (useJustInTimeSeeder) inside.
|
|
86
|
+
const DynamicBlock = ({ block, manifest }: { block: any, manifest: any }) => {
|
|
87
|
+
const Component = getComponent(block.type);
|
|
88
|
+
const safeDataId = block.props.dataId?.toLowerCase();
|
|
89
|
+
|
|
90
|
+
// 1. Find the Schema (Resource Definition)
|
|
91
|
+
const resourceDef = useMemo(() =>
|
|
92
|
+
manifest.resources?.find((r: any) => r.id.toLowerCase() === safeDataId),
|
|
93
|
+
[manifest, safeDataId]);
|
|
94
|
+
|
|
95
|
+
// 2. Fetch Data (Using the Hook!)
|
|
96
|
+
// This automatically checks LocalStorage first, then falls back to JIT generation.
|
|
97
|
+
const resolvedData = useJustInTimeSeeder(safeDataId, resourceDef);
|
|
98
|
+
|
|
99
|
+
// 3. Detect if Data is JIT Generated (for the UI badge)
|
|
100
|
+
// Our JIT seeder prefixes IDs with 'jit_', so we can check that.
|
|
101
|
+
const isGenerated = resolvedData?.[0]?.id?.toString().startsWith('jit_');
|
|
102
|
+
|
|
103
|
+
// 4. Auto-Generate Columns (for SmartTable)
|
|
104
|
+
const autoColumns = useMemo(() => {
|
|
105
|
+
if (safeDataId && block.type === 'SmartTable' && !block.props.columnDefs && resourceDef) {
|
|
106
|
+
return resourceDef.fields.map((f: any) => ({
|
|
107
|
+
field: f.key, headerName: f.label, filter: true, flex: 1,
|
|
108
|
+
cellRenderer: f.type === 'status' ? (p: any) => <Badge variant="secondary">{p.value}</Badge> : undefined
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}, [safeDataId, block.type, resourceDef, block.props.columnDefs]);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div style={{ gridColumn: `span ${block.layout?.colSpan || 1}`, gridRow: `span ${block.layout?.rowSpan || 1}` }} className="relative group">
|
|
116
|
+
<BlockErrorBoundary>
|
|
117
|
+
{/* Developer Debug Overlay */}
|
|
118
|
+
<div className="absolute -top-3 right-0 opacity-0 group-hover:opacity-100 transition-opacity z-20 pointer-events-none translate-y-2 group-hover:translate-y-0 duration-200">
|
|
119
|
+
<div className={`text-[10px] px-2 py-1 rounded-md shadow-lg border border-white/10 flex items-center gap-1.5 font-mono ${isGenerated ? 'bg-amber-600 text-white' : 'bg-slate-800 text-slate-200'}`}>
|
|
120
|
+
<span className="font-bold">{block.type}</span>
|
|
121
|
+
{safeDataId && <> <span className="opacity-40">|</span> {isGenerated ? <Wand2 size={10} className="animate-pulse"/> : <Database size={10}/>} {safeDataId}</>}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<Component
|
|
126
|
+
{...block.props}
|
|
127
|
+
rowData={block.type === 'SmartTable' ? undefined : resolvedData}
|
|
128
|
+
columnDefs={block.props.columnDefs || autoColumns}
|
|
129
|
+
className="w-full h-full"
|
|
130
|
+
/>
|
|
131
|
+
</BlockErrorBoundary>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ------------------------------------------------------------------
|
|
137
|
+
// 4. THE MAIN PAGE COMPONENT
|
|
138
|
+
// ------------------------------------------------------------------
|
|
139
|
+
export const DynamicPage = ({ pageId }: { pageId: string }) => {
|
|
140
|
+
const manifest = useManifest();
|
|
141
|
+
const status = useBridgeStatus();
|
|
142
|
+
const isLive = status === 'live';
|
|
143
|
+
|
|
144
|
+
const page = useMemo(() => manifest.pages?.find((p: any) => p.id === pageId), [manifest, pageId]);
|
|
145
|
+
|
|
146
|
+
if (!page) {
|
|
147
|
+
return (
|
|
148
|
+
<div className="p-8 space-y-4 flex flex-col items-center justify-center h-[50vh]">
|
|
149
|
+
<Loader2 className="animate-spin text-primary mb-4" size={32} />
|
|
150
|
+
<p className="text-muted-foreground">Loading Blueprint...</p>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const statusBadge = (
|
|
156
|
+
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-bold border transition-colors duration-300 ${isLive ? 'bg-green-100 text-green-700 border-green-200' : 'bg-slate-100 text-slate-500 border-slate-200'}`}>
|
|
157
|
+
{isLive ? <Wifi size={14} className="text-green-600 animate-pulse"/> : <WifiOff size={14} />}
|
|
158
|
+
{isLive ? 'LIVE BRIDGE' : 'STATIC MODE'}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="p-6 md:p-8 max-w-7xl mx-auto w-full space-y-8">
|
|
164
|
+
{/* Header */}
|
|
165
|
+
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-border/40 pb-6">
|
|
166
|
+
<div className="space-y-1">
|
|
167
|
+
<h1 className="text-3xl font-bold tracking-tight text-foreground">{page.title}</h1>
|
|
168
|
+
{page.description && <p className="text-muted-foreground">{page.description}</p>}
|
|
169
|
+
</div>
|
|
170
|
+
<div className="flex items-center gap-2">{statusBadge}</div>
|
|
171
|
+
</header>
|
|
172
|
+
|
|
173
|
+
{/* Main Grid */}
|
|
174
|
+
<div className="grid gap-8 pb-20">
|
|
175
|
+
{page.sections?.map((section: any) => (
|
|
176
|
+
<section key={section.id} className="space-y-6">
|
|
177
|
+
{section.title && (
|
|
178
|
+
<div className="flex items-center gap-2 pb-2 border-b border-border/60">
|
|
179
|
+
<h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground/80">{section.title}</h3>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
<div className="grid gap-6" style={{ gridTemplateColumns: `repeat(${section.layout?.columns || 1}, minmax(0, 1fr))` }}>
|
|
183
|
+
{section.blocks.map((block: any) => (
|
|
184
|
+
<DynamicBlock
|
|
185
|
+
key={block.id}
|
|
186
|
+
block={block}
|
|
187
|
+
manifest={manifest}
|
|
188
|
+
/>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
</section>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file route-generator.tsx
|
|
3
|
+
* @description This file is the core engine of the Sitemap-Driven Architecture.
|
|
4
|
+
* Its purpose is to dynamically generate all application routes by reading directly
|
|
5
|
+
* from a designer-friendly `sitemap` array. This abstracts all routing logic
|
|
6
|
+
* away from our designers, empowering them to manage the app's structure from
|
|
7
|
+
* a single, simple configuration file.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Route, Navigate } from 'react-router-dom';
|
|
11
|
+
// CORRECTED IMPORT: Point to the canonical type definition
|
|
12
|
+
import type { SitemapEntry } from './sitemap-entry';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Recursively generates React Router `<Route>` components from a sitemap array.
|
|
16
|
+
* @param {SitemapEntry[]} routes - The array of sitemap entries to process.
|
|
17
|
+
* @returns {JSX.Element[]} An array of React Router `<Route>` components.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const generateRoutes = (routes: SitemapEntry[]) => {
|
|
21
|
+
return routes.map((route) => {
|
|
22
|
+
// Check if the current route is a "parent" route with children
|
|
23
|
+
if (route.children && route.children.length > 0) {
|
|
24
|
+
// Get the path of the first child to use for redirection
|
|
25
|
+
const firstChildPath = route.children[0].path;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
// Create the parent <Route> (e.g., /data)
|
|
29
|
+
<Route key={route.id} path={route.path} element={<route.component />}>
|
|
30
|
+
|
|
31
|
+
{/* PROFESSIONAL PROTOTYPING: This is a key feature for DX.
|
|
32
|
+
It creates an "index" route that automatically redirects the user
|
|
33
|
+
from the parent path (e.g., /data) to its first child (e.g., /data/grid).
|
|
34
|
+
This prevents designers from ever seeing a blank parent page.
|
|
35
|
+
*/}
|
|
36
|
+
<Route index element={<Navigate to={firstChildPath} replace />} />
|
|
37
|
+
|
|
38
|
+
{/* Recursively generate all nested child routes */}
|
|
39
|
+
{generateRoutes(route.children)}
|
|
40
|
+
</Route>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Standard case: This is a simple route with no children.
|
|
45
|
+
return <Route key={route.id} path={route.path} element={<route.component />} />;
|
|
46
|
+
});
|
|
47
|
+
};
|