@ramme-io/create-app 1.1.8 → 1.2.0

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