@ramme-io/create-app 1.2.2 → 1.2.5
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 +9 -14
- package/template/package.json +3 -3
- package/template/pkg.json +9 -7
- package/template/src/App.tsx +11 -1
- package/template/src/components/AppHeader.tsx +10 -10
- package/template/src/components/dashboard/ChartLine.tsx +28 -0
- package/template/src/components/dashboard/StatCard.tsx +61 -0
- package/template/src/config/component-registry.tsx +57 -44
- package/template/src/engine/renderers/DynamicBlock.tsx +1 -1
- package/template/src/engine/renderers/DynamicPage.tsx +134 -98
- package/template/src/engine/runtime/ManifestContext.tsx +79 -0
- package/template/src/engine/runtime/MqttContext.tsx +16 -21
- package/template/src/engine/runtime/data-seeder.ts +9 -7
- package/template/src/engine/runtime/useAction.ts +7 -21
- package/template/src/engine/runtime/useDynamicSitemap.tsx +43 -0
- package/template/src/engine/runtime/useJustInTimeSeeder.ts +76 -0
- package/template/src/engine/runtime/useLiveBridge.ts +44 -0
- package/template/src/engine/runtime/useSignal.ts +10 -21
- package/template/src/engine/runtime/useWorkflowEngine.ts +23 -78
- package/template/src/features/datagrid/SmartTable.tsx +38 -20
- package/template/src/features/developer/GhostOverlay.tsx +67 -21
- package/template/src/features/visualizations/SmartChart.tsx +178 -0
- package/template/src/main.tsx +25 -13
- package/template/src/templates/dashboard/DashboardLayout.tsx +19 -18
- package/template/src/templates/dashboard/dashboard.sitemap.ts +1 -8
- package/template/tailwind.config.cjs +10 -9
- package/template/vite.config.ts +0 -14
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { appManifest as staticManifest } from '../../config/app.manifest';
|
|
3
|
+
import type { AppSpecification } from '../validation/schema';
|
|
4
|
+
|
|
5
|
+
export const useLiveBridge = () => {
|
|
6
|
+
// 1. Initialize with Static Data (Fallback)
|
|
7
|
+
const [manifest, setManifest] = useState<AppSpecification>(staticManifest);
|
|
8
|
+
const [isLive, setIsLive] = useState(false);
|
|
9
|
+
|
|
10
|
+
// Use a ref to ensure listeners aren't duplicated in StrictMode
|
|
11
|
+
const isListening = useRef(false);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (isListening.current) return;
|
|
15
|
+
isListening.current = true;
|
|
16
|
+
|
|
17
|
+
const handleMessage = (event: MessageEvent) => {
|
|
18
|
+
// Optional Security Check: if (event.origin !== "http://localhost:3000") return;
|
|
19
|
+
|
|
20
|
+
if (event.data?.type === 'RAMME_SYNC_MANIFEST') {
|
|
21
|
+
const payload = event.data.payload;
|
|
22
|
+
setManifest(payload);
|
|
23
|
+
setIsLive(true);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
window.addEventListener('message', handleMessage);
|
|
28
|
+
|
|
29
|
+
// ✅ CRITICAL FIX: The Global Handshake Guard
|
|
30
|
+
// Prevents infinite loops if the component remounts
|
|
31
|
+
if (window.parent !== window && !(window as any).__RAMME_HANDSHAKE_SENT) {
|
|
32
|
+
console.log("🔌 [Bridge] Sending Ready Signal...");
|
|
33
|
+
window.parent.postMessage({ type: 'RAMME_CLIENT_READY' }, '*');
|
|
34
|
+
(window as any).__RAMME_HANDSHAKE_SENT = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
window.removeEventListener('message', handleMessage);
|
|
39
|
+
isListening.current = false;
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
return { manifest, isLive };
|
|
44
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import { useSignalStore } from './useSignalStore';
|
|
3
|
-
//
|
|
4
|
-
|
|
3
|
+
// ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
|
|
4
|
+
// ✅ ADDED: Live Context
|
|
5
|
+
import { useManifest } from './ManifestContext';
|
|
5
6
|
|
|
6
7
|
export interface SignalState {
|
|
7
8
|
id: string;
|
|
@@ -10,33 +11,21 @@ export interface SignalState {
|
|
|
10
11
|
min?: number;
|
|
11
12
|
max?: number;
|
|
12
13
|
timestamp?: number;
|
|
13
|
-
status: 'fresh' | 'stale';
|
|
14
|
+
status: 'fresh' | 'stale';
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
* @hook useSignal
|
|
18
|
-
* @description The "Signal Selector".
|
|
19
|
-
*
|
|
20
|
-
* ARCHITECTURAL ROLE:
|
|
21
|
-
* This hook is the bridge between the Static Manifest and the Dynamic Store.
|
|
22
|
-
* 1. It finds the signal definition in the Manifest (for min/max/unit).
|
|
23
|
-
* 2. It grabs the live value from the Zustand SignalStore.
|
|
24
|
-
* 3. It merges them into a single object for the UI to consume.
|
|
25
|
-
*/
|
|
26
17
|
export const useSignal = (signalId: string): SignalState => {
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
const signalData = useSignalStore((state) => state.signals[signalId]);
|
|
19
|
+
|
|
20
|
+
// ✅ 1. Consume Live Manifest
|
|
21
|
+
const appManifest = useManifest();
|
|
29
22
|
|
|
30
|
-
// 2. Get Static Definition from Manifest
|
|
31
|
-
// We use useMemo so we don't search the array on every render
|
|
23
|
+
// 2. Get Static Definition from Manifest (Live)
|
|
32
24
|
const signalDef = useMemo(() => {
|
|
33
25
|
return appManifest.domain.signals.find((s) => s.id === signalId);
|
|
34
|
-
}, [signalId]);
|
|
26
|
+
}, [signalId, appManifest]); // Re-run if manifest updates
|
|
35
27
|
|
|
36
|
-
// 3. Merge and Return
|
|
37
28
|
const value = signalData?.value ?? signalDef?.defaultValue ?? 0;
|
|
38
|
-
|
|
39
|
-
// Determine if data is stale (older than 10 seconds)
|
|
40
29
|
const isStale = signalData ? (Date.now() - signalData.timestamp > 10000) : true;
|
|
41
30
|
|
|
42
31
|
return {
|
|
@@ -1,122 +1,69 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import { useToast } from '@ramme-io/ui';
|
|
3
|
-
// ✅ 1. Correct Imports from Store
|
|
4
3
|
import { useGeneratedSignals, useSimulation } from './useSignalStore';
|
|
5
|
-
//
|
|
6
|
-
|
|
4
|
+
// ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
|
|
5
|
+
// ✅ ADDED: Live Context
|
|
6
|
+
import { useManifest } from './ManifestContext';
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
interface ActionDefinition {
|
|
10
|
-
|
|
11
|
-
config: Record<string, any>;
|
|
12
|
-
}
|
|
8
|
+
// ... (Interfaces can remain the same) ...
|
|
9
|
+
interface ActionDefinition { type: string; config: Record<string, any>; }
|
|
10
|
+
interface WorkflowDefinition { id: string; name: string; active: boolean; trigger: { type: string; config: { signalId: string; condition: string; }; }; actions: ActionDefinition[]; }
|
|
13
11
|
|
|
14
|
-
interface WorkflowDefinition {
|
|
15
|
-
id: string;
|
|
16
|
-
name: string;
|
|
17
|
-
active: boolean;
|
|
18
|
-
trigger: {
|
|
19
|
-
type: string;
|
|
20
|
-
config: {
|
|
21
|
-
signalId: string;
|
|
22
|
-
condition: string;
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
actions: ActionDefinition[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* THE RUNTIME LOGIC ENGINE
|
|
30
|
-
* This hook breathes life into the application.
|
|
31
|
-
* It watches for signal changes and executes the workflows defined in the manifest.
|
|
32
|
-
*/
|
|
33
12
|
export const useWorkflowEngine = () => {
|
|
34
13
|
const signals = useGeneratedSignals();
|
|
35
14
|
const { addToast } = useToast();
|
|
36
15
|
|
|
37
|
-
// 1.
|
|
38
|
-
|
|
16
|
+
// ✅ 1. Consume Live Manifest
|
|
17
|
+
const appManifest = useManifest();
|
|
18
|
+
|
|
19
|
+
// 2. Activate Simulation (Responsive to toggle)
|
|
39
20
|
useSimulation(appManifest.config.mockMode);
|
|
40
21
|
|
|
41
|
-
// 2. Define the Action Executor
|
|
42
22
|
const executeAction = async (action: ActionDefinition, context: any) => {
|
|
23
|
+
// ... (Same execution logic as before) ...
|
|
43
24
|
console.log(`[Engine] Executing: ${action.type}`, action);
|
|
44
|
-
|
|
45
25
|
switch (action.type) {
|
|
46
|
-
case 'send_notification':
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case 'update_resource':
|
|
51
|
-
// In a real app, this would call the API
|
|
52
|
-
addToast(`Updating Resource: ${JSON.stringify(action.config)}`, 'success');
|
|
53
|
-
break;
|
|
54
|
-
|
|
55
|
-
case 'agent_task':
|
|
56
|
-
// Simulate AI Agent processing
|
|
57
|
-
console.log(`[AI Agent] Thinking about: "${action.config.prompt}"...`);
|
|
26
|
+
case 'send_notification': addToast(action.config.message || 'Notification Sent', 'info'); break;
|
|
27
|
+
case 'update_resource': addToast(`Updating Resource: ${JSON.stringify(action.config)}`, 'success'); break;
|
|
28
|
+
case 'navigate': window.location.href = action.config.path; break;
|
|
29
|
+
case 'agent_task':
|
|
58
30
|
addToast('AI Agent Analyzing...', 'info');
|
|
59
|
-
|
|
60
|
-
setTimeout(() => {
|
|
61
|
-
// Mock response based on prompt context
|
|
62
|
-
const response = action.config.prompt?.includes('health')
|
|
63
|
-
? "System Operating Normally."
|
|
64
|
-
: "Optimization Recommended.";
|
|
65
|
-
|
|
66
|
-
addToast(`🤖 Agent: "${response}"`, 'success', 5000);
|
|
67
|
-
}, 1500);
|
|
68
|
-
break;
|
|
69
|
-
|
|
70
|
-
case 'navigate':
|
|
71
|
-
window.location.href = action.config.path;
|
|
31
|
+
setTimeout(() => addToast('🤖 Agent: "System Nominal"', 'success', 3000), 1500);
|
|
72
32
|
break;
|
|
73
|
-
|
|
74
|
-
default:
|
|
75
|
-
console.warn('Unknown action type:', action.type);
|
|
76
33
|
}
|
|
77
34
|
};
|
|
78
35
|
|
|
79
|
-
// 3. Watch Signals & Trigger Workflows
|
|
36
|
+
// 3. Watch Signals & Trigger Workflows (Live)
|
|
80
37
|
useEffect(() => {
|
|
81
|
-
// Safety check for undefined domain/workflows
|
|
82
38
|
if (!appManifest.domain?.workflows) return;
|
|
83
39
|
|
|
84
|
-
// We cast to our local type since manifest might be "any"
|
|
85
40
|
(appManifest.domain.workflows as unknown as WorkflowDefinition[]).forEach((flow) => {
|
|
86
41
|
if (!flow.active) return;
|
|
87
42
|
|
|
88
|
-
// Check Trigger: Signal Change
|
|
89
43
|
if (flow.trigger.type === 'signal_change') {
|
|
90
44
|
const signalId = flow.trigger.config.signalId;
|
|
91
|
-
const condition = flow.trigger.config.condition;
|
|
45
|
+
const condition = flow.trigger.config.condition;
|
|
92
46
|
|
|
47
|
+
// @ts-ignore
|
|
93
48
|
const signal = signals[signalId];
|
|
94
49
|
|
|
95
50
|
if (signal) {
|
|
96
|
-
const val = signal.value;
|
|
97
|
-
|
|
51
|
+
const val = signal.value;
|
|
98
52
|
try {
|
|
99
|
-
// Check if condition is met (e.g. "50 > 80")
|
|
100
53
|
const isMet = checkCondition(val, condition);
|
|
101
|
-
|
|
102
|
-
// Debounce: In a real app, we'd check timestamps to avoid firing every 2ms
|
|
103
|
-
// For now, we rely on the simulation being slow (2s)
|
|
104
54
|
if (isMet) {
|
|
105
55
|
console.log(`[Engine] Trigger Fired: ${flow.name}`);
|
|
106
56
|
flow.actions.forEach(action => executeAction(action, { signal: val }));
|
|
107
57
|
}
|
|
108
|
-
} catch (e) {
|
|
109
|
-
// Ignore parse errors
|
|
110
|
-
}
|
|
58
|
+
} catch (e) {}
|
|
111
59
|
}
|
|
112
60
|
}
|
|
113
61
|
});
|
|
114
|
-
}, [signals, addToast]);
|
|
62
|
+
}, [signals, addToast, appManifest.domain.workflows]); // Added dependency
|
|
115
63
|
|
|
116
64
|
return {
|
|
117
|
-
// Exposed for manual triggering (e.g. Buttons)
|
|
118
65
|
triggerWorkflow: (workflowId: string) => {
|
|
119
|
-
//
|
|
66
|
+
// ✅ 4. Manual Triggers now find new workflows instantly
|
|
120
67
|
const flow = appManifest.domain?.workflows?.find(w => w.id === workflowId);
|
|
121
68
|
if (flow) {
|
|
122
69
|
// @ts-ignore
|
|
@@ -126,13 +73,11 @@ export const useWorkflowEngine = () => {
|
|
|
126
73
|
};
|
|
127
74
|
};
|
|
128
75
|
|
|
129
|
-
// --- HELPER: Safe Condition Checker ---
|
|
130
76
|
const checkCondition = (value: number, condition: string): boolean => {
|
|
131
77
|
if (!condition) return false;
|
|
132
78
|
const parts = condition.trim().split(' ');
|
|
133
79
|
const operator = parts[0];
|
|
134
80
|
const target = parseFloat(parts[1]);
|
|
135
|
-
|
|
136
81
|
switch (operator) {
|
|
137
82
|
case '>': return value > target;
|
|
138
83
|
case '<': return value < target;
|
|
@@ -9,9 +9,12 @@ import {
|
|
|
9
9
|
type ColDef,
|
|
10
10
|
type GridApi
|
|
11
11
|
} from '@ramme-io/ui';
|
|
12
|
-
import {
|
|
12
|
+
import { useJustInTimeSeeder } from '../../engine/runtime/useJustInTimeSeeder';
|
|
13
|
+
import { getResourceMeta } from '../../data/mockData';
|
|
13
14
|
import { AutoForm } from '../../components/AutoForm';
|
|
14
15
|
import { useCrudLocalStorage } from '../../engine/runtime/useCrudLocalStorage';
|
|
16
|
+
import { useManifest } from '../../engine/runtime/ManifestContext';
|
|
17
|
+
import type { ResourceDefinition } from '../../engine/validation/schema';
|
|
15
18
|
|
|
16
19
|
interface SmartTableProps {
|
|
17
20
|
dataId: string;
|
|
@@ -24,11 +27,37 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
24
27
|
title}) => {
|
|
25
28
|
const { addToast } = useToast();
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const manifest = useManifest();
|
|
31
|
+
|
|
32
|
+
// ✅ FIX: Normalize 'meta' to always match 'ResourceDefinition'
|
|
33
|
+
const meta = useMemo<ResourceDefinition | null>(() => {
|
|
34
|
+
// 1. Try Dynamic Manifest (Preview Mode)
|
|
35
|
+
const dynamicResource = manifest.resources?.find((r: ResourceDefinition) => r.id === dataId);
|
|
36
|
+
|
|
37
|
+
if (dynamicResource) {
|
|
38
|
+
return dynamicResource;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Try Static Data (Deployed Mode)
|
|
42
|
+
const staticMeta = getResourceMeta(dataId);
|
|
43
|
+
|
|
44
|
+
if (staticMeta) {
|
|
45
|
+
// ⚠️ Type Patch: Inject 'id' so it satisfies ResourceDefinition
|
|
46
|
+
// The static file uses the object key as the ID, but the type expects it inline.
|
|
47
|
+
return {
|
|
48
|
+
...staticMeta,
|
|
49
|
+
id: dataId,
|
|
50
|
+
// Ensure 'type' strings from mockData match the Zod enum if needed,
|
|
51
|
+
// but typically 'text' | 'number' overlaps fine.
|
|
52
|
+
} as unknown as ResourceDefinition;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}, [manifest, dataId]);
|
|
57
|
+
|
|
58
|
+
// ✅ JIT Seeder now receives a valid ResourceDefinition
|
|
59
|
+
const seedData = useJustInTimeSeeder(dataId, meta);
|
|
30
60
|
|
|
31
|
-
// We use the CRUD hook to persist changes to localStorage
|
|
32
61
|
const {
|
|
33
62
|
data: rowData,
|
|
34
63
|
createItem,
|
|
@@ -36,14 +65,14 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
36
65
|
deleteItem
|
|
37
66
|
} = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
|
|
38
67
|
|
|
39
|
-
//
|
|
68
|
+
// --- UI STATE ---
|
|
40
69
|
const [isEditOpen, setIsEditOpen] = useState(false);
|
|
41
70
|
const [currentRecord, setCurrentRecord] = useState<any>(null);
|
|
42
71
|
const [gridApi, setGridApi] = useState<GridApi | null>(null);
|
|
43
72
|
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
44
73
|
const [quickFilterText, setQuickFilterText] = useState('');
|
|
45
74
|
|
|
46
|
-
//
|
|
75
|
+
// --- COLUMN DEFINITIONS ---
|
|
47
76
|
const columns = useMemo<ColDef[]>(() => {
|
|
48
77
|
if (!meta?.fields) return [];
|
|
49
78
|
|
|
@@ -57,7 +86,6 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
57
86
|
flex: 1,
|
|
58
87
|
};
|
|
59
88
|
|
|
60
|
-
// Smart Formatting based on type
|
|
61
89
|
if (f.type === 'currency') {
|
|
62
90
|
col.valueFormatter = (p: any) => p.value ? `$${Number(p.value).toLocaleString()}` : '';
|
|
63
91
|
}
|
|
@@ -84,14 +112,12 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
84
112
|
return col;
|
|
85
113
|
});
|
|
86
114
|
|
|
87
|
-
// Add Checkbox Selection to the first column
|
|
88
115
|
if (generatedCols.length > 0) {
|
|
89
116
|
generatedCols[0].headerCheckboxSelection = true;
|
|
90
117
|
generatedCols[0].checkboxSelection = true;
|
|
91
118
|
generatedCols[0].minWidth = 180;
|
|
92
119
|
}
|
|
93
120
|
|
|
94
|
-
// Add Actions Column
|
|
95
121
|
generatedCols.push({
|
|
96
122
|
headerName: "Actions",
|
|
97
123
|
field: "id",
|
|
@@ -109,7 +135,7 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
109
135
|
return generatedCols;
|
|
110
136
|
}, [meta]);
|
|
111
137
|
|
|
112
|
-
//
|
|
138
|
+
// --- HANDLERS ---
|
|
113
139
|
const onGridReady = useCallback((params: any) => {
|
|
114
140
|
setGridApi(params.api);
|
|
115
141
|
}, []);
|
|
@@ -140,14 +166,10 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
140
166
|
setIsEditOpen(false);
|
|
141
167
|
};
|
|
142
168
|
|
|
143
|
-
// --- RENDER ---
|
|
144
169
|
return (
|
|
145
170
|
<Card className="flex flex-col h-[600px] border border-border shadow-sm overflow-hidden bg-card">
|
|
146
171
|
|
|
147
|
-
{/* HEADER TOOLBAR */}
|
|
148
172
|
<div className="p-4 border-b border-border flex justify-between items-center gap-4 bg-muted/5">
|
|
149
|
-
|
|
150
|
-
{/* Left: Title or Bulk Actions */}
|
|
151
173
|
{selectedRows.length > 0 ? (
|
|
152
174
|
<div className="flex items-center gap-3 animate-in fade-in slide-in-from-left-2 duration-200">
|
|
153
175
|
<span className="bg-primary text-primary-foreground text-xs font-bold px-2 py-1 rounded-md">
|
|
@@ -173,7 +195,6 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
173
195
|
</div>
|
|
174
196
|
)}
|
|
175
197
|
|
|
176
|
-
{/* Right: Actions & Filter */}
|
|
177
198
|
<div className="flex items-center gap-2">
|
|
178
199
|
<div className="w-64">
|
|
179
200
|
<SearchInput
|
|
@@ -181,8 +202,7 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
181
202
|
value={quickFilterText}
|
|
182
203
|
onChange={(e) => {
|
|
183
204
|
setQuickFilterText(e.target.value);
|
|
184
|
-
gridApi?.
|
|
185
|
-
}}
|
|
205
|
+
gridApi?.updateGridOptions({ quickFilterText: e.target.value }); }}
|
|
186
206
|
/>
|
|
187
207
|
</div>
|
|
188
208
|
<div className="h-6 w-px bg-border mx-1" />
|
|
@@ -192,7 +212,6 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
192
212
|
</div>
|
|
193
213
|
</div>
|
|
194
214
|
|
|
195
|
-
{/* AG GRID */}
|
|
196
215
|
<div className="flex-1 w-full bg-card relative">
|
|
197
216
|
<DataTable
|
|
198
217
|
rowData={rowData}
|
|
@@ -208,7 +227,6 @@ export const SmartTable: React.FC<SmartTableProps> = ({
|
|
|
208
227
|
/>
|
|
209
228
|
</div>
|
|
210
229
|
|
|
211
|
-
{/* EDIT DRAWER */}
|
|
212
230
|
<AutoForm
|
|
213
231
|
isOpen={isEditOpen}
|
|
214
232
|
onClose={() => setIsEditOpen(false)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Icon } from '@ramme-io/ui';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Icon, cn } from '@ramme-io/ui';
|
|
3
3
|
|
|
4
4
|
interface GhostOverlayProps {
|
|
5
5
|
children: React.ReactNode;
|
|
@@ -16,51 +16,97 @@ export const GhostOverlay: React.FC<GhostOverlayProps> = ({
|
|
|
16
16
|
signalId,
|
|
17
17
|
isActive = false,
|
|
18
18
|
}) => {
|
|
19
|
-
|
|
19
|
+
const [isRemoteSelected, setIsRemoteSelected] = useState(false);
|
|
20
|
+
|
|
21
|
+
// --- 1. INBOUND BRIDGE: Listen for Builder Selections ---
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!isActive) return;
|
|
24
|
+
|
|
25
|
+
const handleMessage = (event: MessageEvent) => {
|
|
26
|
+
// Security Check: In prod, you'd check event.origin
|
|
27
|
+
const { type, payload } = event.data || {};
|
|
28
|
+
|
|
29
|
+
if (type === 'RAMME_HIGHLIGHT_BLOCK') {
|
|
30
|
+
// If the Builder says "Highlight Block X", checking if it's us
|
|
31
|
+
setIsRemoteSelected(payload?.blockId === componentId);
|
|
32
|
+
|
|
33
|
+
// Optional: Scroll into view if selected remotely
|
|
34
|
+
if (payload?.blockId === componentId) {
|
|
35
|
+
document.getElementById(`ghost-${componentId}`)?.scrollIntoView({
|
|
36
|
+
behavior: 'smooth',
|
|
37
|
+
block: 'center'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
window.addEventListener('message', handleMessage);
|
|
44
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
45
|
+
}, [isActive, componentId]);
|
|
46
|
+
|
|
20
47
|
if (!isActive) {
|
|
21
48
|
return <>{children}</>;
|
|
22
49
|
}
|
|
23
50
|
|
|
51
|
+
// --- 2. OUTBOUND BRIDGE: Signal the Builder ---
|
|
24
52
|
const handleClick = (e: React.MouseEvent) => {
|
|
25
|
-
// 🛑 Stop the click from triggering
|
|
53
|
+
// 🛑 Zero Jank: Stop the click from triggering links/buttons
|
|
26
54
|
e.preventDefault();
|
|
27
55
|
e.stopPropagation();
|
|
28
56
|
|
|
29
|
-
// 🚀
|
|
57
|
+
// 🚀 Signal the Parent
|
|
30
58
|
window.parent.postMessage({
|
|
31
59
|
type: 'RAMME_SELECT_BLOCK',
|
|
32
|
-
payload: { blockId: componentId }
|
|
60
|
+
payload: { blockId: componentId, type: componentType }
|
|
33
61
|
}, '*');
|
|
62
|
+
|
|
63
|
+
// Optimistic UI update (optional, usually we wait for parent to confirm)
|
|
64
|
+
setIsRemoteSelected(true);
|
|
34
65
|
};
|
|
35
66
|
|
|
36
67
|
return (
|
|
37
68
|
<div
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
id={`ghost-${componentId}`}
|
|
70
|
+
className="relative group isolate" // isolate creates new stacking context
|
|
71
|
+
onClick={handleClick}
|
|
40
72
|
>
|
|
41
|
-
{/*
|
|
42
|
-
<div className=
|
|
73
|
+
{/* --- THE GHOST LAYER --- */}
|
|
74
|
+
<div className={cn(
|
|
75
|
+
"absolute inset-0 z-50 rounded-lg transition-all duration-200 pointer-events-auto cursor-pointer",
|
|
76
|
+
// Default State (Hover)
|
|
77
|
+
"hover:ring-2 hover:ring-primary/50 hover:bg-primary/5",
|
|
78
|
+
// Selected State (Remote or Local)
|
|
79
|
+
isRemoteSelected ? "ring-2 ring-primary bg-primary/10 shadow-[0_0_0_4px_rgba(var(--app-primary-color),0.1)]" : "border-2 border-dashed border-transparent"
|
|
80
|
+
)} />
|
|
43
81
|
|
|
44
|
-
{/*
|
|
45
|
-
<div className=
|
|
46
|
-
|
|
47
|
-
|
|
82
|
+
{/* --- INFO BADGE (Top Left) --- */}
|
|
83
|
+
<div className={cn(
|
|
84
|
+
"absolute top-0 left-0 z-50 p-2 transform -translate-y-1/2 transition-opacity duration-200",
|
|
85
|
+
isRemoteSelected || "group-hover:opacity-100 opacity-0"
|
|
86
|
+
)}>
|
|
87
|
+
<div className="flex items-center gap-2 bg-foreground text-background text-[10px] font-mono py-1 px-2 rounded shadow-sm border border-border">
|
|
88
|
+
<Icon name="box" size={10} />
|
|
48
89
|
<span className="font-bold">{componentType}</span>
|
|
90
|
+
<span className="opacity-50">#{componentId.slice(0, 4)}</span>
|
|
49
91
|
</div>
|
|
50
92
|
</div>
|
|
51
93
|
|
|
52
|
-
{/*
|
|
94
|
+
{/* --- DATA WIRE (Bottom Right) --- */}
|
|
53
95
|
{signalId && (
|
|
54
|
-
<div className=
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
96
|
+
<div className={cn(
|
|
97
|
+
"absolute bottom-0 right-0 z-50 p-2 transform translate-y-1/2 transition-opacity duration-200",
|
|
98
|
+
isRemoteSelected || "group-hover:opacity-100 opacity-0"
|
|
99
|
+
)}>
|
|
100
|
+
<div className="flex items-center gap-1.5 bg-accent text-accent-foreground text-[10px] font-mono py-1 px-2 rounded shadow-sm border border-accent/20">
|
|
101
|
+
<Icon name="activity" size={10} />
|
|
102
|
+
<span>{signalId}</span>
|
|
58
103
|
</div>
|
|
59
104
|
</div>
|
|
60
105
|
)}
|
|
61
106
|
|
|
62
|
-
{/*
|
|
63
|
-
|
|
107
|
+
{/* --- CONTENT (Frozen) --- */}
|
|
108
|
+
{/* We disable pointer events on children so buttons don't fire while Ghost is active */}
|
|
109
|
+
<div className="pointer-events-none select-none">
|
|
64
110
|
{children}
|
|
65
111
|
</div>
|
|
66
112
|
</div>
|