@ramme-io/create-app 1.2.2 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/package.json +9 -14
  2. package/template/package.json +5 -3
  3. package/template/pkg.json +11 -7
  4. package/template/src/App.tsx +11 -1
  5. package/template/src/components/AppHeader.tsx +10 -10
  6. package/template/src/components/dashboard/ChartLine.tsx +28 -0
  7. package/template/src/components/dashboard/StatCard.tsx +61 -0
  8. package/template/src/config/component-registry.tsx +57 -44
  9. package/template/src/engine/renderers/DynamicBlock.tsx +1 -1
  10. package/template/src/engine/renderers/DynamicPage.tsx +134 -98
  11. package/template/src/engine/runtime/ManifestContext.tsx +79 -0
  12. package/template/src/engine/runtime/MqttContext.tsx +16 -21
  13. package/template/src/engine/runtime/data-seeder.ts +9 -7
  14. package/template/src/engine/runtime/useAction.ts +7 -21
  15. package/template/src/engine/runtime/useDynamicSitemap.tsx +43 -0
  16. package/template/src/engine/runtime/useJustInTimeSeeder.ts +76 -0
  17. package/template/src/engine/runtime/useLiveBridge.ts +44 -0
  18. package/template/src/engine/runtime/useSignal.ts +10 -21
  19. package/template/src/engine/runtime/useWorkflowEngine.ts +23 -78
  20. package/template/src/features/datagrid/SmartTable.tsx +38 -20
  21. package/template/src/features/developer/GhostOverlay.tsx +67 -21
  22. package/template/src/features/visualizations/SmartChart.tsx +178 -0
  23. package/template/src/main.tsx +25 -13
  24. package/template/src/templates/dashboard/DashboardLayout.tsx +19 -18
  25. package/template/src/templates/dashboard/dashboard.sitemap.ts +1 -8
  26. package/template/tailwind.config.cjs +10 -9
  27. 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
- // Import Manifest to get static metadata (min, max, units)
4
- import { appManifest } from '../../config/app.manifest';
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'; // Simplified status
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
- // 1. Get Live Data from Store
28
- const signalData = useSignalStore((state: { signals: { [x: string]: any; }; }) => state.signals[signalId]);
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
- // 2. Correct Import from Manifest
6
- import { appManifest } from '../../config/app.manifest';
4
+ // REMOVED: import { appManifest } from '../../config/app.manifest';
5
+ // ADDED: Live Context
6
+ import { useManifest } from './ManifestContext';
7
7
 
8
- // Minimal Types for internal use (until schema is formalized)
9
- interface ActionDefinition {
10
- type: string;
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. Activate the Data Simulation (Randomizer)
38
- // This now checks internally if it should run (based on boolean arg)
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
- addToast(action.config.message || 'Notification Sent', 'info');
48
- break;
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; // e.g., "> 80"
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; // Cleaned up access
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
- // @ts-ignore
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 { getResourceMeta, getMockData } from '../../data/mockData';
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
- // 1. DATA KERNEL
28
- const meta = getResourceMeta(dataId);
29
- const seedData = useMemo(() => getMockData(dataId) || [], [dataId]);
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
- // 2. UI STATE
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
- // 3. COLUMN DEFINITIONS
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
- // 4. HANDLERS
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?.setQuickFilter(e.target.value);
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 app logic (like navigation or toggles)
53
+ // 🛑 Zero Jank: Stop the click from triggering links/buttons
26
54
  e.preventDefault();
27
55
  e.stopPropagation();
28
56
 
29
- // 🚀 THE GHOST BRIDGE: Signal the Parent (The Builder)
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
- className="relative group cursor-pointer"
39
- onClick={handleClick} // Add Click Handler
69
+ id={`ghost-${componentId}`}
70
+ className="relative group isolate" // isolate creates new stacking context
71
+ onClick={handleClick}
40
72
  >
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" />
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
- {/* 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">
46
- <div className="flex items-center gap-2 bg-accent text-accent-foreground text-xs font-mono py-1 px-2 rounded shadow-sm">
47
- <Icon name="box" size={12} />
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
- {/* The "Signal Wire" */}
94
+ {/* --- DATA WIRE (Bottom Right) --- */}
53
95
  {signalId && (
54
- <div className="absolute bottom-0 right-0 z-50 p-2 transform translate-y-1/2 -translate-x-2 pointer-events-none">
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">
56
- <Icon name="activity" size={12} />
57
- <span>{signalId}</span>
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
- {/* Render the actual component underneath */}
63
- <div className="opacity-50 grayscale transition-all duration-200 group-hover:opacity-75 group-hover:grayscale-0 pointer-events-none">
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>