@ramme-io/create-app 1.1.9 → 1.2.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.
- package/package.json +4 -3
- package/template/pkg.json +16 -13
- package/template/src/App.tsx +17 -11
- package/template/src/blocks/SmartTable.tsx +224 -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 +64 -58
- package/template/src/hooks/useDataQuery.ts +84 -0
- package/template/src/hooks/useSignal.ts +43 -33
- package/template/src/hooks/useWorkflowEngine.ts +123 -0
- package/template/src/pages/Dashboard.tsx +43 -90
- package/template/src/pages/DynamicPage.tsx +54 -22
- package/template/src/pages/Welcome.tsx +162 -0
- package/template/src/templates/dashboard/DashboardLayout.tsx +2 -0
- package/template/src/templates/dashboard/dashboard.sitemap.ts +33 -72
- package/template/src/types/schema.ts +117 -30
|
@@ -1,61 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
// --- 1. SIGNAL STORE ---
|
|
5
|
+
export interface SignalValue {
|
|
6
|
+
value: any;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SignalStore {
|
|
11
|
+
signals: Record<string, SignalValue>;
|
|
12
|
+
updateSignal: (id: string, value: any) => void;
|
|
13
|
+
updateSignals: (updates: Record<string, any>) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Initial State matches app.manifest.ts
|
|
17
|
+
const initialState = {
|
|
18
|
+
living_room_ac: { value: 72, timestamp: Date.now() },
|
|
19
|
+
living_room_hum: { value: 45, timestamp: Date.now() },
|
|
20
|
+
server_01: { value: 42, timestamp: Date.now() },
|
|
21
|
+
front_door_lock: { value: 'LOCKED', timestamp: Date.now() }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const useSignalStore = create<SignalStore>((set) => ({
|
|
25
|
+
signals: initialState,
|
|
26
|
+
|
|
27
|
+
updateSignal: (id, value) => set((state) => ({
|
|
28
|
+
signals: {
|
|
29
|
+
...state.signals,
|
|
30
|
+
[id]: { value, timestamp: Date.now() }
|
|
31
|
+
}
|
|
32
|
+
})),
|
|
33
|
+
|
|
34
|
+
updateSignals: (updates) => set((state) => {
|
|
35
|
+
const newSignals = { ...state.signals };
|
|
36
|
+
Object.entries(updates).forEach(([id, val]) => {
|
|
37
|
+
newSignals[id] = { value: val, timestamp: Date.now() };
|
|
38
|
+
});
|
|
39
|
+
return { signals: newSignals };
|
|
40
|
+
})
|
|
41
|
+
}));
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
export const useGeneratedSignals = () => {
|
|
44
|
+
return useSignalStore((state) => state.signals);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// --- 2. SIMULATION ENGINE ---
|
|
48
|
+
export const useSimulation = () => {
|
|
49
|
+
const { updateSignals } = useSignalStore();
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
console.log("[System] Simulation Started");
|
|
53
|
+
const interval = setInterval(() => {
|
|
54
|
+
// Simulate random fluctuations for sensors
|
|
55
|
+
const updates: Record<string, any> = {};
|
|
26
56
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
...prev,
|
|
35
|
-
'mrr_stripe': {
|
|
36
|
-
value: getNestedValue(json, 'data.finance.mrr'),
|
|
37
|
-
unit: 'USD',
|
|
38
|
-
status: 'fresh'
|
|
39
|
-
},
|
|
40
|
-
'active_users': {
|
|
41
|
-
value: getNestedValue(json, 'data.users.total'),
|
|
42
|
-
unit: '',
|
|
43
|
-
status: 'fresh'
|
|
44
|
-
}
|
|
45
|
-
}));
|
|
46
|
-
}
|
|
47
|
-
} catch (err) {
|
|
48
|
-
console.error("API Polling Error:", err);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// Initial Fetch
|
|
53
|
-
pollEndpoints();
|
|
54
|
-
|
|
55
|
-
// Poll every 5s
|
|
56
|
-
const interval = setInterval(pollEndpoints, 5000);
|
|
57
|
-
return () => clearInterval(interval);
|
|
58
|
-
}, []);
|
|
57
|
+
// Randomize values slightly
|
|
58
|
+
updates['living_room_ac'] = Number((72 + (Math.random() * 4 - 2)).toFixed(1));
|
|
59
|
+
updates['living_room_hum'] = Number((45 + (Math.random() * 6 - 3)).toFixed(1));
|
|
60
|
+
updates['server_01'] = Math.floor(Math.random() * 100);
|
|
61
|
+
|
|
62
|
+
updateSignals(updates);
|
|
63
|
+
}, 2000); // Update every 2 seconds
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
}
|
|
65
|
+
return () => clearInterval(interval);
|
|
66
|
+
}, [updateSignals]);
|
|
67
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
// --- 1. Export the Missing Types (Fixes ts(2305)) ---
|
|
4
|
+
|
|
5
|
+
export type SortDirection = 'asc' | 'desc';
|
|
6
|
+
|
|
7
|
+
export interface SortOption {
|
|
8
|
+
field: string;
|
|
9
|
+
direction: SortDirection;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FilterOption {
|
|
13
|
+
field: string;
|
|
14
|
+
operator: 'equals' | 'contains' | 'gt' | 'lt' | 'neq';
|
|
15
|
+
value: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QueryOptions {
|
|
19
|
+
filters?: FilterOption[];
|
|
20
|
+
sort?: SortOption;
|
|
21
|
+
page?: number;
|
|
22
|
+
pageSize?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface QueryResult<T> {
|
|
26
|
+
data: T[];
|
|
27
|
+
total: number;
|
|
28
|
+
pageCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- 2. Update the Hook Signature (Fixes ts(2345)) ---
|
|
32
|
+
|
|
33
|
+
export function useDataQuery<T>(
|
|
34
|
+
rawData: T[], // <--- Now accepts an Array, NOT a string ID
|
|
35
|
+
options: QueryOptions = {}
|
|
36
|
+
): QueryResult<T> {
|
|
37
|
+
const { filters, sort, page = 1, pageSize = 10 } = options;
|
|
38
|
+
|
|
39
|
+
// A. Filtering Logic
|
|
40
|
+
const filteredData = useMemo(() => {
|
|
41
|
+
if (!filters || filters.length === 0) return rawData;
|
|
42
|
+
|
|
43
|
+
return rawData.filter((item: any) => {
|
|
44
|
+
return filters.every((filter) => {
|
|
45
|
+
const itemValue = item[filter.field];
|
|
46
|
+
|
|
47
|
+
switch (filter.operator) {
|
|
48
|
+
case 'equals': return itemValue == filter.value;
|
|
49
|
+
case 'neq': return itemValue != filter.value;
|
|
50
|
+
case 'contains':
|
|
51
|
+
return String(itemValue).toLowerCase().includes(String(filter.value).toLowerCase());
|
|
52
|
+
case 'gt': return itemValue > filter.value;
|
|
53
|
+
case 'lt': return itemValue < filter.value;
|
|
54
|
+
default: return true;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}, [rawData, filters]);
|
|
59
|
+
|
|
60
|
+
// B. Sorting Logic
|
|
61
|
+
const sortedData = useMemo(() => {
|
|
62
|
+
if (!sort) return filteredData;
|
|
63
|
+
|
|
64
|
+
return [...filteredData].sort((a: any, b: any) => {
|
|
65
|
+
const aValue = a[sort.field];
|
|
66
|
+
const bValue = b[sort.field];
|
|
67
|
+
if (aValue < bValue) return sort.direction === 'asc' ? -1 : 1;
|
|
68
|
+
if (aValue > bValue) return sort.direction === 'asc' ? 1 : -1;
|
|
69
|
+
return 0;
|
|
70
|
+
});
|
|
71
|
+
}, [filteredData, sort]);
|
|
72
|
+
|
|
73
|
+
// C. Pagination Logic
|
|
74
|
+
const paginatedResult = useMemo(() => {
|
|
75
|
+
const startIndex = (page - 1) * pageSize;
|
|
76
|
+
return sortedData.slice(startIndex, startIndex + pageSize);
|
|
77
|
+
}, [sortedData, page, pageSize]);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
data: paginatedResult,
|
|
81
|
+
total: filteredData.length,
|
|
82
|
+
pageCount: Math.ceil(filteredData.length / pageSize),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -1,73 +1,83 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useMqtt } from '../contexts/MqttContext';
|
|
2
3
|
import type { Signal } from '../types/signal';
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
* Configuration for the Mock Generator.
|
|
6
|
-
* This allows us to simulate specific hardware constraints.
|
|
7
|
-
*/
|
|
8
5
|
interface SignalConfig<T> {
|
|
9
6
|
initialValue?: T;
|
|
10
|
-
min?: number;
|
|
11
|
-
max?: number;
|
|
12
|
-
interval?: number; //
|
|
13
|
-
unit?: string;
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
interval?: number; // Mock mode only
|
|
10
|
+
unit?: string;
|
|
11
|
+
topic?: string; // Real mode only
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
/**
|
|
17
|
-
* The "Universal Socket."
|
|
18
|
-
* UI Components use this hook to subscribe to data.
|
|
19
|
-
* Currently running in "Synthetic Mode" (Mock) with bounded randomization.
|
|
20
|
-
*/
|
|
21
14
|
export function useSignal<T = any>(signalId: string, config: SignalConfig<T> = {}): Signal<T> {
|
|
22
|
-
// Defaults
|
|
23
15
|
const {
|
|
24
16
|
initialValue,
|
|
25
17
|
min = -Infinity,
|
|
26
18
|
max = Infinity,
|
|
27
19
|
interval = 2000,
|
|
28
|
-
unit
|
|
20
|
+
unit,
|
|
21
|
+
topic
|
|
29
22
|
} = config;
|
|
30
23
|
|
|
31
|
-
|
|
24
|
+
const { subscribe, unsubscribe, lastMessage, isConnected } = useMqtt();
|
|
25
|
+
|
|
32
26
|
const [signal, setSignal] = useState<Signal<T>>({
|
|
33
27
|
id: signalId,
|
|
34
28
|
value: initialValue as T,
|
|
35
29
|
unit: unit,
|
|
36
30
|
timestamp: Date.now(),
|
|
37
31
|
status: 'fresh',
|
|
32
|
+
max: max
|
|
38
33
|
});
|
|
39
34
|
|
|
35
|
+
// --- REAL MODE: MQTT ---
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!topic || !isConnected) return;
|
|
38
|
+
subscribe(topic);
|
|
39
|
+
return () => unsubscribe(topic);
|
|
40
|
+
}, [topic, isConnected, subscribe, unsubscribe]);
|
|
41
|
+
|
|
40
42
|
useEffect(() => {
|
|
41
|
-
|
|
43
|
+
if (!topic || !lastMessage[topic]) return;
|
|
44
|
+
|
|
45
|
+
const rawValue = lastMessage[topic];
|
|
46
|
+
let parsedValue: any = rawValue;
|
|
47
|
+
|
|
48
|
+
// Auto-parse numbers and booleans
|
|
49
|
+
if (!isNaN(Number(rawValue))) parsedValue = Number(rawValue);
|
|
50
|
+
else if (rawValue.toLowerCase() === 'true' || rawValue === 'on') parsedValue = true;
|
|
51
|
+
else if (rawValue.toLowerCase() === 'false' || rawValue === 'off') parsedValue = false;
|
|
52
|
+
|
|
53
|
+
setSignal(prev => ({
|
|
54
|
+
...prev,
|
|
55
|
+
value: parsedValue,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
status: 'fresh'
|
|
58
|
+
}));
|
|
59
|
+
}, [lastMessage, topic]);
|
|
60
|
+
|
|
61
|
+
// --- MOCK MODE: SIMULATION ---
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (topic) return; // Disable mock if topic exists
|
|
64
|
+
|
|
42
65
|
const timer = setInterval(() => {
|
|
43
66
|
setSignal(prev => {
|
|
44
67
|
let newValue: any = prev.value;
|
|
45
|
-
|
|
46
|
-
// Only apply math if it's a number
|
|
47
68
|
if (typeof prev.value === 'number') {
|
|
48
|
-
|
|
49
|
-
const variance = (Math.random() - 0.5) * 2; // +/- 1
|
|
69
|
+
const variance = (Math.random() - 0.5) * 2;
|
|
50
70
|
let nextNum = prev.value + variance;
|
|
51
|
-
|
|
52
|
-
// 🛡️ CLAMPING: Apply the physical bounds
|
|
53
71
|
if (min !== undefined) nextNum = Math.max(min, nextNum);
|
|
54
72
|
if (max !== undefined) nextNum = Math.min(max, nextNum);
|
|
55
|
-
|
|
56
73
|
newValue = Number(nextNum.toFixed(1));
|
|
57
74
|
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
...prev,
|
|
61
|
-
value: newValue,
|
|
62
|
-
timestamp: Date.now(),
|
|
63
|
-
status: 'fresh',
|
|
64
|
-
unit: unit // Ensure unit persists
|
|
65
|
-
};
|
|
75
|
+
return { ...prev, value: newValue, timestamp: Date.now(), status: 'fresh' };
|
|
66
76
|
});
|
|
67
77
|
}, interval);
|
|
68
78
|
|
|
69
79
|
return () => clearInterval(timer);
|
|
70
|
-
}, [
|
|
80
|
+
}, [topic, min, max, interval]);
|
|
71
81
|
|
|
72
82
|
return signal;
|
|
73
83
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useToast } from '@ramme-io/ui';
|
|
3
|
+
// @ts-ignore - These generated hooks exist in the build
|
|
4
|
+
import { useGeneratedSignals, useSimulation } from '../generated/hooks';
|
|
5
|
+
import { appManifest } from '../config/app.manifest';
|
|
6
|
+
// ✅ Import types to fix implicit 'any' errors
|
|
7
|
+
import type { ActionDefinition, WorkflowDefinition } from '../types/schema';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* THE RUNTIME LOGIC ENGINE
|
|
11
|
+
* This hook breathes life into the application.
|
|
12
|
+
* It watches for signal changes and executes the workflows defined in the manifest.
|
|
13
|
+
*/
|
|
14
|
+
export const useWorkflowEngine = () => {
|
|
15
|
+
const signals = useGeneratedSignals();
|
|
16
|
+
const { addToast } = useToast();
|
|
17
|
+
|
|
18
|
+
// 1. Activate the Data Simulation (Randomizer)
|
|
19
|
+
useSimulation();
|
|
20
|
+
|
|
21
|
+
// 2. Define the Action Executor
|
|
22
|
+
// ✅ Explicitly typed 'action'
|
|
23
|
+
const executeAction = async (action: ActionDefinition, context: any) => {
|
|
24
|
+
console.log(`[Engine] Executing: ${action.type}`, action);
|
|
25
|
+
|
|
26
|
+
switch (action.type) {
|
|
27
|
+
case 'send_notification':
|
|
28
|
+
addToast(action.config.message || 'Notification Sent', 'info');
|
|
29
|
+
break;
|
|
30
|
+
|
|
31
|
+
case 'update_resource':
|
|
32
|
+
// In a real app, this would call the API
|
|
33
|
+
addToast(`Updating Resource: ${JSON.stringify(action.config)}`, 'success');
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
case 'agent_task':
|
|
37
|
+
// Simulate AI Agent processing
|
|
38
|
+
console.log(`[AI Agent] Thinking about: "${action.config.prompt}"...`);
|
|
39
|
+
// ✅ FIX: 'loading' is not a valid ToastType. Using 'info' instead.
|
|
40
|
+
addToast('AI Agent Analyzing...', 'info');
|
|
41
|
+
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
// Mock response based on prompt context
|
|
44
|
+
const response = action.config.prompt?.includes('health')
|
|
45
|
+
? "System Operating Normally."
|
|
46
|
+
: "Optimization Recommended.";
|
|
47
|
+
|
|
48
|
+
addToast(`🤖 Agent: "${response}"`, 'success', 5000);
|
|
49
|
+
}, 1500);
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case 'navigate':
|
|
53
|
+
window.location.href = action.config.path;
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
default:
|
|
57
|
+
console.warn('Unknown action type:', action.type);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 3. Watch Signals & Trigger Workflows
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!appManifest.domain?.workflows) return;
|
|
64
|
+
|
|
65
|
+
// ✅ Explicitly typed 'flow'
|
|
66
|
+
appManifest.domain.workflows.forEach((flow: WorkflowDefinition) => {
|
|
67
|
+
if (!flow.active) return;
|
|
68
|
+
|
|
69
|
+
// Check Trigger: Signal Change
|
|
70
|
+
if (flow.trigger.type === 'signal_change') {
|
|
71
|
+
const signalId = flow.trigger.config.signalId;
|
|
72
|
+
const condition = flow.trigger.config.condition; // e.g., "> 80"
|
|
73
|
+
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
const signal = signals[signalId];
|
|
76
|
+
|
|
77
|
+
if (signal) {
|
|
78
|
+
const val = typeof signal === 'object' ? signal.value : signal;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Check if condition is met (e.g. "50 > 80")
|
|
82
|
+
const isMet = checkCondition(val, condition);
|
|
83
|
+
|
|
84
|
+
if (isMet) {
|
|
85
|
+
// Throttling logic would go here to prevent spam
|
|
86
|
+
console.log(`[Engine] Trigger Fired: ${flow.name}`);
|
|
87
|
+
flow.actions.forEach(action => executeAction(action, { signal: val }));
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Ignore parse errors
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}, [signals, addToast]);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
// Exposed for manual triggering (e.g. Buttons)
|
|
99
|
+
triggerWorkflow: (workflowId: string) => {
|
|
100
|
+
const flow = appManifest.domain?.workflows?.find(w => w.id === workflowId);
|
|
101
|
+
if (flow) {
|
|
102
|
+
flow.actions.forEach(action => executeAction(action, { manual: true }));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// --- HELPER: Safe Condition Checker ---
|
|
109
|
+
const checkCondition = (value: number, condition: string): boolean => {
|
|
110
|
+
if (!condition) return false;
|
|
111
|
+
const parts = condition.trim().split(' ');
|
|
112
|
+
const operator = parts[0];
|
|
113
|
+
const target = parseFloat(parts[1]);
|
|
114
|
+
|
|
115
|
+
switch (operator) {
|
|
116
|
+
case '>': return value > target;
|
|
117
|
+
case '<': return value < target;
|
|
118
|
+
case '>=': return value >= target;
|
|
119
|
+
case '<=': return value <= target;
|
|
120
|
+
case '==': return value === target;
|
|
121
|
+
default: return false;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
@@ -1,105 +1,58 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import { appManifest } from '../config/app.manifest';
|
|
7
|
-
import { useGeneratedSignals } from '../generated/hooks';
|
|
8
|
-
import type { EntityDefinition } from '../types/schema';
|
|
9
|
-
import { useAction } from '../hooks/useAction';
|
|
10
|
-
|
|
11
|
-
// --- DEV TOOLS (Restored) ---
|
|
12
|
-
import { useDevTools } from '../hooks/useDevTools';
|
|
13
|
-
import { GhostOverlay } from '../components/dev/GhostOverlay';
|
|
2
|
+
import { useLocation } from 'react-router-dom';
|
|
3
|
+
import { PageHeader, Alert } from '@ramme-io/ui';
|
|
4
|
+
import { appManifest } from '../config/app.manifest';
|
|
5
|
+
import { DynamicBlock } from '../components/DynamicBlock';
|
|
14
6
|
|
|
15
7
|
const Dashboard: React.FC = () => {
|
|
16
|
-
|
|
17
|
-
const signals = useGeneratedSignals();
|
|
18
|
-
const { meta, domain } = appManifest;
|
|
8
|
+
const location = useLocation();
|
|
19
9
|
|
|
20
|
-
//
|
|
21
|
-
const
|
|
22
|
-
|
|
10
|
+
// 1. Determine current slug from URL
|
|
11
|
+
const pathParts = location.pathname.split('/').filter(Boolean);
|
|
12
|
+
// If path is root or /dashboard, slug is 'dashboard', else take the last part
|
|
13
|
+
const currentSlug = pathParts.length > 1 ? pathParts[pathParts.length - 1] : 'dashboard';
|
|
14
|
+
|
|
15
|
+
// 2. Find the matching Page Definition
|
|
16
|
+
const pageDef = appManifest.pages?.find(p => p.slug === currentSlug)
|
|
17
|
+
|| appManifest.pages?.[0];
|
|
18
|
+
|
|
19
|
+
if (!pageDef) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="p-8">
|
|
22
|
+
<Alert variant="warning" title="Page Not Found">
|
|
23
|
+
Could not find a definition for slug: <code>{currentSlug}</code>
|
|
24
|
+
</Alert>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
return (
|
|
25
30
|
<div className="space-y-8 relative">
|
|
26
31
|
<PageHeader
|
|
27
|
-
title={
|
|
28
|
-
description={
|
|
29
|
-
actions={
|
|
30
|
-
// 3. Restore the Toggle Button
|
|
31
|
-
<Button
|
|
32
|
-
variant={isGhostMode ? 'accent' : 'outline'}
|
|
33
|
-
size="sm"
|
|
34
|
-
onClick={toggleGhostMode}
|
|
35
|
-
title="Toggle Ghost Mode (Ctrl+Shift+G)"
|
|
36
|
-
>
|
|
37
|
-
<Icon name={isGhostMode ? 'eye' : 'eye-off'} className="mr-2" />
|
|
38
|
-
{isGhostMode ? 'Ghost Mode: ON' : 'Dev Tools'}
|
|
39
|
-
</Button>
|
|
40
|
-
}
|
|
32
|
+
title={pageDef.title}
|
|
33
|
+
description={pageDef.description}
|
|
41
34
|
/>
|
|
42
35
|
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
{pageDef.sections.map((section) => (
|
|
37
|
+
<div key={section.id} className="relative">
|
|
38
|
+
{section.title && (
|
|
39
|
+
<h3 className="text-lg font-semibold mb-4 text-foreground">
|
|
40
|
+
{section.title}
|
|
41
|
+
</h3>
|
|
42
|
+
)}
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// C. Inject Signal Data
|
|
63
|
-
if (signal) {
|
|
64
|
-
dynamicProps.value = `${signal.value} ${signal.unit || ''}`;
|
|
65
|
-
dynamicProps.status = 'active';
|
|
66
|
-
|
|
67
|
-
if (componentType === 'ToggleCard') {
|
|
68
|
-
dynamicProps.children = (
|
|
69
|
-
<div className="flex items-center justify-between mt-2">
|
|
70
|
-
<span className="text-sm text-muted-foreground">Active</span>
|
|
71
|
-
<ToggleSwitch
|
|
72
|
-
label="Toggle"
|
|
73
|
-
checked={(signal.value as any) === true || signal.value === 'true' || signal.value === 1}
|
|
74
|
-
// THE UPDATE:
|
|
75
|
-
onChange={(val) => sendAction(entity.id, val)}
|
|
76
|
-
/>
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
delete dynamicProps.value;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
// 4. Restore the GhostOverlay Wrapper
|
|
85
|
-
<GhostOverlay
|
|
86
|
-
key={entity.id}
|
|
87
|
-
isActive={isGhostMode}
|
|
88
|
-
componentId={entity.id}
|
|
89
|
-
componentType={componentType}
|
|
90
|
-
signalId={primarySignalId}
|
|
91
|
-
>
|
|
92
|
-
<Component {...dynamicProps} />
|
|
93
|
-
</GhostOverlay>
|
|
94
|
-
);
|
|
95
|
-
})}
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
{domain.entities.length === 0 && (
|
|
99
|
-
<div className="p-12 text-center border-2 border-dashed border-slate-200 rounded-lg text-slate-400">
|
|
100
|
-
<p>No entities defined.</p>
|
|
44
|
+
<div
|
|
45
|
+
className="grid gap-6"
|
|
46
|
+
style={{
|
|
47
|
+
gridTemplateColumns: `repeat(${section.layout?.columns || 3}, minmax(300px, 1fr))`
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{section.blocks.map((block) => (
|
|
51
|
+
<DynamicBlock key={block.id} block={block} />
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
101
54
|
</div>
|
|
102
|
-
)}
|
|
55
|
+
))}
|
|
103
56
|
</div>
|
|
104
57
|
);
|
|
105
58
|
};
|