@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
@@ -1,114 +1,150 @@
1
- import React from 'react';
2
- import { PageHeader, Alert, Button, Icon } from '@ramme-io/ui';
3
- import { appManifest } from '../../config/app.manifest';
4
- // Reuse the GhostOverlay for structure visualization
5
- import { GhostOverlay } from '../../features/developer/GhostOverlay';
6
- // Import the Worker Bee
7
- import { DynamicBlock } from './DynamicBlock';
8
- // Import the Hook to control the overlay
9
- import { useDevTools } from '../../features/developer/useDevTools';
1
+ import React, { Component, type ErrorInfo, useMemo } from 'react';
2
+ import { PageHeader, Alert, Badge } from '@ramme-io/ui';
3
+ import { useManifest, useBridgeStatus } from '../runtime/ManifestContext';
4
+ import { getComponent } from '../../config/component-registry';
5
+ import { getMockData } from '../../data/mockData'; // Keep as fallback
6
+ import { Wifi, WifiOff, AlertTriangle, Loader2, Database, Wand2 } from 'lucide-react';
10
7
 
11
- /**
12
- * @file DynamicPage.tsx
13
- * @description The "Layout Engine" of the application.
14
- *
15
- * ARCHITECTURAL ROLE:
16
- * This component is the bridge between the Abstract JSON Manifest and the Concrete DOM.
17
- * It acts as the "General Contractor," taking the blueprint (Manifest) and instructing
18
- * the workers (DynamicBlock) where to build.
19
- *
20
- * HIERARCHY:
21
- * App -> Routes -> DynamicPage -> (Iterates Sections) -> DynamicBlock -> (Actual Component)
22
- *
23
- * KEY FEATURES:
24
- * 1. **Manifest Lookup:** Uses the `pageId` prop to find the correct configuration in `app.manifest.ts`.
25
- * 2. **Grid Composition:** Translates abstract layout numbers (e.g., `columns: 3`) into real CSS Grid styles.
26
- * 3. **DevTools Integration:** Wraps blocks in `GhostOverlay` (when enabled) to visualize the
27
- * hidden boundaries and signal connections during development.
28
- */
8
+ // --- 🛠️ JIT DATA GENERATOR ---
9
+ // This ensures the preview ALWAYS has data, even for new resources not yet in the DB.
10
+ const generateJitData = (resourceDef: any, count = 10) => {
11
+ if (!resourceDef) return [];
12
+
13
+ return Array.from({ length: count }).map((_, i) => {
14
+ const row: any = { id: i + 1 };
15
+ resourceDef.fields.forEach((f: any) => {
16
+ // Intelligent Mocking based on field type
17
+ if (f.type === 'status') {
18
+ row[f.key] = ['Active', 'Pending', 'Closed', 'Archived'][Math.floor(Math.random() * 4)];
19
+ } else if (f.type === 'boolean') {
20
+ row[f.key] = Math.random() > 0.3; // 70% true
21
+ } else if (f.type === 'number' || f.type === 'currency') {
22
+ row[f.key] = Math.floor(Math.random() * 1000) + 100;
23
+ } else if (f.type === 'date') {
24
+ const d = new Date();
25
+ d.setDate(d.getDate() - Math.floor(Math.random() * 30));
26
+ row[f.key] = d.toISOString().split('T')[0];
27
+ } else {
28
+ row[f.key] = `${f.label} ${i + 1}`;
29
+ }
30
+ });
31
+ return row;
32
+ });
33
+ };
29
34
 
30
- interface DynamicPageProps {
31
- pageId: string;
35
+ // --- ERROR BOUNDARY ---
36
+ class BlockErrorBoundary extends Component<{ children: React.ReactNode }, { hasError: boolean, error: string }> {
37
+ constructor(props: any) { super(props); this.state = { hasError: false, error: '' }; }
38
+ static getDerivedStateFromError(error: any) { return { hasError: true, error: error.message }; }
39
+ render() {
40
+ if (this.state.hasError) return (
41
+ <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">
42
+ <AlertTriangle size={16} className="mb-2 opacity-80" />
43
+ <span className="font-bold">Render Error</span>
44
+ <span className="opacity-75 text-center truncate max-w-[200px]">{this.state.error}</span>
45
+ </div>
46
+ );
47
+ return this.props.children;
48
+ }
32
49
  }
33
50
 
34
- export const DynamicPage: React.FC<DynamicPageProps> = ({ pageId }) => {
35
- // 1. Initialize DevTools
36
- const { isGhostMode, toggleGhostMode } = useDevTools();
51
+ // --- MAIN RENDERER ---
52
+ export const DynamicPage = ({ pageId }: { pageId: string }) => {
53
+ const manifest = useManifest();
54
+ const status = useBridgeStatus();
55
+ const isLive = status === 'live';
37
56
 
38
- // 2. Look up the page definition
39
- const pageConfig = (appManifest as any).pages?.find((p: any) => p.id === pageId);
57
+ const page = useMemo(() => manifest.pages?.find((p: any) => p.id === pageId), [manifest, pageId]);
40
58
 
41
- if (!pageConfig) {
59
+ if (!page) {
42
60
  return (
43
- <div className="p-8">
44
- <Alert variant="danger" title="Page Not Found">
45
- The manifest does not contain a page with ID: <code>{pageId}</code>.
46
- </Alert>
61
+ <div className="p-8 space-y-4">
62
+ <Alert variant="info" title="Connecting..."><Loader2 className="animate-spin mr-2" size={16} /> Loading Page...</Alert>
47
63
  </div>
48
64
  );
49
65
  }
50
66
 
51
67
  return (
52
- <div className="space-y-8 fade-in p-6">
53
- <PageHeader
54
- title={pageConfig.title}
55
- description={pageConfig.description}
56
- actions={
57
- <Button
58
- variant={isGhostMode ? 'accent' : 'outline'}
59
- size="sm"
60
- onClick={toggleGhostMode}
61
- title="Toggle Ghost Mode (Ctrl+Shift+G)"
62
- >
63
- <Icon name={isGhostMode ? 'eye' : 'eye-off'} className="mr-2" />
64
- {isGhostMode ? 'Ghost Mode: ON' : 'Dev Tools'}
65
- </Button>
66
- }
67
- />
68
+ <div className="space-y-8 animate-in fade-in duration-300 pb-20 p-6 md:p-8">
69
+ {/* Header */}
70
+ <div className="flex flex-col md:flex-row md:items-start justify-between gap-4 border-b border-border pb-6">
71
+ <div>
72
+ <h1 className="text-3xl font-bold tracking-tight text-foreground">{page.title}</h1>
73
+ {page.description && <p className="text-muted-foreground text-lg">{page.description}</p>}
74
+ </div>
75
+ <div className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-bold border ${isLive ? 'bg-green-100 text-green-700 border-green-200' : 'bg-slate-100 text-slate-500'}`}>
76
+ {isLive ? <Wifi size={14} className="text-green-600 animate-pulse"/> : <WifiOff size={14} />}
77
+ {isLive ? 'LIVE PREVIEW' : 'STATIC MODE'}
78
+ </div>
79
+ </div>
80
+
81
+ {/* Grid Layout */}
82
+ <div className="grid gap-8">
83
+ {page.sections?.map((section: any) => (
84
+ <section key={section.id} className="space-y-4">
85
+ {section.title && (
86
+ <div className="flex items-center gap-2 pb-2 border-b border-dashed border-gray-200">
87
+ <h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground">{section.title}</h3>
88
+ </div>
89
+ )}
90
+ <div className="grid gap-6" style={{ gridTemplateColumns: `repeat(${section.layout?.columns || 1}, minmax(0, 1fr))` }}>
91
+ {section.blocks.map((block: any) => {
92
+ const Component = getComponent(block.type);
93
+ const safeDataId = block.props.dataId?.toLowerCase();
94
+
95
+ // --- 🛡️ HYBRID DATA STRATEGY ---
96
+ let resolvedData: any[] = [];
97
+ let isGenerated = false;
98
+ let autoColumns = undefined;
68
99
 
69
- {/* 4. Render Sections */}
70
- {pageConfig.sections.map((section: any) => (
71
- <div key={section.id} className="space-y-4">
72
- {section.title && <h3 className="text-xl font-semibold">{section.title}</h3>}
73
-
74
- <div
75
- className="grid gap-6"
76
- style={{
77
- // ⚡️ IMPROVEMENT: Default to 1 column (Full Width) instead of 3.
78
- // This fixes the "narrow table" issue immediately.
79
- gridTemplateColumns: `repeat(${section.layout?.columns || 1}, minmax(0, 1fr))`
80
- }}
81
- >
82
- {section.blocks.map((block: any) => {
83
- // ⚡️ IMPROVEMENT: Calculate Spanning
84
- // Allows a block to stretch across multiple columns if needed.
85
- const colSpan = block.layout?.colSpan || 1;
86
- const rowSpan = block.layout?.rowSpan || 1;
100
+ if (safeDataId) {
101
+ // 1. Try Local Storage first
102
+ resolvedData = getMockData(safeDataId);
103
+
104
+ // 2. If empty, Generate JIT Data from Manifest
105
+ if (!resolvedData || resolvedData.length === 0) {
106
+ const resourceDef = manifest.resources?.find((r: any) => r.id.toLowerCase() === safeDataId);
107
+ if (resourceDef) {
108
+ resolvedData = generateJitData(resourceDef);
109
+ isGenerated = true; // Flag for debug overlay
110
+ }
111
+ }
87
112
 
88
- return (
89
- <div
90
- key={block.id}
91
- style={{
92
- gridColumn: `span ${colSpan}`,
93
- gridRow: `span ${rowSpan}`
94
- }}
95
- >
96
- <GhostOverlay
97
- isActive={isGhostMode}
98
- componentId={block.id}
99
- componentType={block.type}
100
- signalId={block.props.signalId}
101
- >
102
- <DynamicBlock block={block} />
103
- </GhostOverlay>
104
- </div>
105
- );
106
- })}
107
- </div>
108
- </div>
109
- ))}
113
+ // 3. Auto-Generate Table Columns
114
+ const resourceDef = manifest.resources?.find((r: any) => r.id.toLowerCase() === safeDataId);
115
+ if (resourceDef && !block.props.columnDefs) {
116
+ autoColumns = resourceDef.fields.map((f: any) => ({
117
+ field: f.key, headerName: f.label, filter: true, flex: 1,
118
+ cellRenderer: f.type === 'status' ? (p: any) => <Badge variant="secondary">{p.value}</Badge> : undefined
119
+ }));
120
+ }
121
+ }
122
+
123
+ return (
124
+ <div key={block.id} style={{ gridColumn: `span ${block.layout?.colSpan || 1}`, gridRow: `span ${block.layout?.rowSpan || 1}` }} className="relative group">
125
+ <BlockErrorBoundary>
126
+ {/* Debug Overlay */}
127
+ <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-20 flex gap-1 pointer-events-none">
128
+ <div className={`text-white text-[10px] px-2 py-1 rounded backdrop-blur-md flex items-center gap-1 ${isGenerated ? 'bg-amber-600/90' : 'bg-black/80'}`}>
129
+ {block.type}
130
+ {safeDataId && <> <span className="opacity-50">|</span> {isGenerated ? <Wand2 size={10}/> : <Database size={10}/>} {safeDataId} ({resolvedData.length})</>}
131
+ </div>
132
+ </div>
133
+
134
+ <Component
135
+ {...block.props}
136
+ rowData={resolvedData}
137
+ columnDefs={block.props.columnDefs || autoColumns}
138
+ className="w-full h-full shadow-sm bg-card rounded-xl border border-border"
139
+ />
140
+ </BlockErrorBoundary>
141
+ </div>
142
+ );
143
+ })}
144
+ </div>
145
+ </section>
146
+ ))}
147
+ </div>
110
148
  </div>
111
149
  );
112
- };
113
-
114
- export default DynamicPage;
150
+ };
@@ -0,0 +1,79 @@
1
+ import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
2
+ // ✅ FIX 1: Correct Path to Static Manifest
3
+ import { appManifest as staticManifest } from '../../config/app.manifest';
4
+ // ✅ FIX 2: Correct Path to Types
5
+ import type { AppSpecification } from '../validation/schema';
6
+
7
+ interface ManifestContextType {
8
+ manifest: AppSpecification;
9
+ isLive: boolean;
10
+ }
11
+
12
+ const ManifestContext = createContext<ManifestContextType>({
13
+ manifest: staticManifest,
14
+ isLive: false
15
+ });
16
+
17
+ export const ManifestProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
18
+ // 1. Initialize State
19
+ const [manifest, setManifest] = useState<AppSpecification>(staticManifest);
20
+ const [isLive, setIsLive] = useState(false);
21
+
22
+ // 2. Global Listener Guard (Refs persist across re-renders)
23
+ const isListening = useRef(false);
24
+
25
+ useEffect(() => {
26
+ // Prevent double-attaching listeners in StrictMode
27
+ if (isListening.current) return;
28
+ isListening.current = true;
29
+
30
+ // A. Define the Listener
31
+ const handleMessage = (event: MessageEvent) => {
32
+ // Security: Filter out noise, listen only for our specific event
33
+ if (event.data?.type === 'RAMME_SYNC_MANIFEST') {
34
+ const payload = event.data.payload;
35
+ // console.log("⚡️ [Context] Sync Received");
36
+ setManifest(payload);
37
+ setIsLive(true);
38
+ }
39
+ };
40
+
41
+ // B. Attach Listener
42
+ window.addEventListener('message', handleMessage);
43
+
44
+ // C. Send Handshake (ONCE per session)
45
+ // We check a global window property to ensure we never spam the parent,
46
+ // even if this Provider is unmounted and remounted by React Router.
47
+ if (window.parent !== window && !(window as any).__RAMME_HANDSHAKE_SENT) {
48
+ console.log("🔌 [Context] Handshake Sent 🤝");
49
+ window.parent.postMessage({ type: 'RAMME_CLIENT_READY' }, '*');
50
+ (window as any).__RAMME_HANDSHAKE_SENT = true;
51
+ }
52
+
53
+ // Cleanup
54
+ return () => {
55
+ window.removeEventListener('message', handleMessage);
56
+ isListening.current = false;
57
+ };
58
+ }, []);
59
+
60
+ return (
61
+ <ManifestContext.Provider value={{ manifest, isLive }}>
62
+ {children}
63
+ </ManifestContext.Provider>
64
+ );
65
+ };
66
+
67
+ // --- CONSUMER HOOKS ---
68
+
69
+ export const useManifest = () => {
70
+ const context = useContext(ManifestContext);
71
+ if (!context) return staticManifest;
72
+ return context.manifest;
73
+ };
74
+
75
+ // Returns 'live' | 'static' string to match your UI components
76
+ export const useBridgeStatus = () => {
77
+ const context = useContext(ManifestContext);
78
+ return context?.isLive ? 'live' : 'static';
79
+ };
@@ -1,6 +1,8 @@
1
1
  import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
2
2
  import mqtt, { type MqttClient } from 'mqtt';
3
- import { appManifest } from '../../config/app.manifest'; // ✅ Connect to the Single Source of Truth
3
+ // ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
4
+ // ✅ ADDED: Live Context
5
+ import { useManifest } from './ManifestContext';
4
6
 
5
7
  interface MqttContextType {
6
8
  isConnected: boolean;
@@ -12,44 +14,38 @@ interface MqttContextType {
12
14
 
13
15
  const MqttContext = createContext<MqttContextType | null>(null);
14
16
 
15
- /**
16
- * @file MqttContext.tsx
17
- * @description The Real-Time Connectivity Layer.
18
- *
19
- * ARCHITECTURAL ROLE:
20
- * This provider establishes a persistent WebSocket connection to the MQTT Broker
21
- * defined in `app.manifest.ts`.
22
- *
23
- * KEY FEATURES:
24
- * 1. **Global Connection:** Maintains one connection for the whole app (Singleton pattern).
25
- * 2. **Topic Management:** specific components (like DeviceCard) can subscribe to
26
- * specific topics on demand using `subscribe()`.
27
- * 3. **State Distribution:** Broadcasts the latest messages to any component using `useMqtt()`.
28
- */
29
-
30
17
  export const MqttProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
31
18
  const [isConnected, setIsConnected] = useState(false);
32
19
  const [lastMessage, setLastMessage] = useState<Record<string, string>>({});
33
20
  const clientRef = useRef<MqttClient | null>(null);
34
21
  const subscriptions = useRef<Set<string>>(new Set());
35
22
 
23
+ // ✅ 1. Consume Live Manifest
24
+ const appManifest = useManifest();
25
+
36
26
  useEffect(() => {
37
- // ✅ Load Broker URL from the Manifest Config
27
+ // ✅ 2. Hot-Swap Broker Connection
28
+ // If you change the Broker URL in the Builder, this effect will re-run!
38
29
  const brokerUrl = appManifest.config.brokerUrl || 'wss://test.mosquitto.org:8081';
39
30
  console.log(`[MQTT] Connecting to ${brokerUrl}...`);
40
31
 
32
+ // Disconnect previous if exists
33
+ if (clientRef.current) {
34
+ clientRef.current.end();
35
+ }
36
+
41
37
  const client = mqtt.connect(brokerUrl);
42
38
  clientRef.current = client;
43
39
 
44
40
  client.on('connect', () => {
45
41
  console.log('[MQTT] Connected ✅');
46
42
  setIsConnected(true);
43
+ // Re-subscribe to previous topics if needed
44
+ subscriptions.current.forEach(t => client.subscribe(t));
47
45
  });
48
46
 
49
- // ✅ FIX: Proper typing for MQTT payload (Buffer)
50
47
  client.on('message', (topic: string, payload: Buffer) => {
51
48
  const messageStr = payload.toString();
52
- // console.log(`[MQTT] Msg: ${topic} -> ${messageStr}`); // Optional debug
53
49
  setLastMessage((prev) => ({ ...prev, [topic]: messageStr }));
54
50
  });
55
51
 
@@ -62,11 +58,10 @@ export const MqttProvider: React.FC<{ children: React.ReactNode }> = ({ children
62
58
  console.log('[MQTT] Disconnecting...');
63
59
  client.end();
64
60
  };
65
- }, []);
61
+ }, [appManifest.config.brokerUrl]); // Only re-connect if URL changes
66
62
 
67
63
  const subscribe = (topic: string) => {
68
64
  if (clientRef.current && !subscriptions.current.has(topic)) {
69
- console.log(`[MQTT] Subscribing to: ${topic}`);
70
65
  clientRef.current.subscribe(topic);
71
66
  subscriptions.current.add(topic);
72
67
  }
@@ -1,5 +1,3 @@
1
- // src/core/data-seeder.ts
2
-
3
1
  // ✅ Match the export from your new mockData.ts
4
2
  import { DATA_REGISTRY } from '../../data/mockData';
5
3
 
@@ -10,21 +8,23 @@ import { DATA_REGISTRY } from '../../data/mockData';
10
8
  * This utility ensures that the LocalStorage "Data Lake" is never empty.
11
9
  * On app launch, it checks if data exists. If not, it writes the seed data
12
10
  * from `mockData.ts` into the browser's storage.
13
- * * This allows the app to feel "alive" with data immediately after installation.
14
11
  */
15
12
 
13
+ // 🔒 SHARED CONSTANT: Ensure everyone uses the same key format
14
+ const DB_PREFIX = 'ramme_mock_';
15
+
16
16
  export const initializeDataLake = () => {
17
17
  if (typeof window === 'undefined') return;
18
18
 
19
19
  console.groupCollapsed('🌊 [Data Lake] Initialization');
20
20
 
21
21
  Object.entries(DATA_REGISTRY).forEach(([key, seedData]) => {
22
- // ⚠️ REMOVED PREFIX ('ramme_db_') so it matches AuthContext
23
- const storageKey = key;
22
+ // FIX: Use the prefix so getMockData() can find it
23
+ const storageKey = `${DB_PREFIX}${key}`;
24
24
  const existing = localStorage.getItem(storageKey);
25
25
 
26
26
  if (!existing) {
27
- console.log(`✨ Seeding collection: ${key} (${seedData.length} records)`);
27
+ console.log(`✨ Seeding collection: ${key} (${(seedData as any[]).length} records)`);
28
28
  localStorage.setItem(storageKey, JSON.stringify(seedData));
29
29
  } else {
30
30
  console.log(`✅ Collection exists: ${key}`);
@@ -39,7 +39,9 @@ export const initializeDataLake = () => {
39
39
  */
40
40
  export const resetDataLake = () => {
41
41
  Object.keys(DATA_REGISTRY).forEach((key) => {
42
- localStorage.removeItem(key); // Matches the storageKey above
42
+ const storageKey = `${DB_PREFIX}${key}`;
43
+ localStorage.removeItem(storageKey);
43
44
  });
45
+ console.log("🔥 Data Lake Evaporated (Cleared)");
44
46
  window.location.reload();
45
47
  };
@@ -1,36 +1,25 @@
1
1
  import { useCallback } from 'react';
2
2
  import { useMqtt } from './MqttContext';
3
- // User Verified Path
4
- import { appManifest } from '../../config/app.manifest';
5
-
6
- /**
7
- * @file useAction.ts
8
- * @description The "Effectuator" hook.
9
- *
10
- * ARCHITECTURAL ROLE:
11
- * This hook handles outgoing commands from the UI. It abstracts the transport layer,
12
- * automatically routing actions to the correct destination (MQTT, HTTP, or Mock Console)
13
- * based on the entity's configuration in the manifest.
14
- */
3
+ // REMOVED: import { appManifest } from '../../config/app.manifest';
4
+ // ADDED: Live Context
5
+ import { useManifest } from './ManifestContext';
15
6
 
16
7
  export const useAction = () => {
17
8
  const { publish, isConnected } = useMqtt();
18
9
 
19
- // Destructure config/domain from the manifest
10
+ // 1. Consume Live Manifest
11
+ const appManifest = useManifest();
20
12
  const { config, domain } = appManifest;
21
13
 
22
14
  const sendAction = useCallback(async (entityId: string, value: any) => {
23
- // 1. Find the Entity definition
24
- // Note: If domain.entities is empty (early stage), this is where we'd add fallback logic
15
+ // 2. Find the Entity definition (Live)
25
16
  const entity = domain.entities.find(e => e.id === entityId);
26
17
 
27
18
  if (!entity) {
28
- // For development/debugging, we log this even if the entity is missing
29
19
  console.warn(`[Action] Entity ID '${entityId}' not found in manifest.`);
30
20
  return;
31
21
  }
32
22
 
33
- // 2. Find the Primary Signal (The target of the action)
34
23
  const signalId = entity.signals[0];
35
24
  const signal = domain.signals.find(s => s.id === signalId);
36
25
 
@@ -39,8 +28,6 @@ export const useAction = () => {
39
28
  return;
40
29
  }
41
30
 
42
- // 3. EXECUTE based on Mode & Source
43
-
44
31
  // --- Mock Mode ---
45
32
  if (config.mockMode) {
46
33
  console.log(`%c[Mock Action] Setting ${entity.name} to:`, 'color: #10b981; font-weight: bold;', value);
@@ -60,7 +47,6 @@ export const useAction = () => {
60
47
 
61
48
  // --- Live Mode (HTTP) ---
62
49
  if (signal.source === 'http' && signal.endpoint) {
63
- console.log(`[HTTP] POST to ${signal.endpoint}:`, value);
64
50
  try {
65
51
  await fetch(signal.endpoint, {
66
52
  method: 'POST',
@@ -68,7 +54,7 @@ export const useAction = () => {
68
54
  body: JSON.stringify({ id: signal.id, value })
69
55
  });
70
56
  } catch (err) {
71
- console.log('[HTTP] (Simulation) Request sent.');
57
+ console.log('[HTTP] (Simulation) Request sent.');
72
58
  }
73
59
  }
74
60
 
@@ -0,0 +1,43 @@
1
+ import { useMemo } from 'react';
2
+ import { useManifest, useBridgeStatus } from './ManifestContext';
3
+ import { DynamicPage } from '../renderers/DynamicPage';
4
+ import { type SitemapEntry } from '../types/sitemap-entry';
5
+ import { type IconName } from '@ramme-io/ui';
6
+
7
+ /**
8
+ * @hook useDynamicSitemap
9
+ * @description
10
+ * - In 'Static Mode' (npm run dev): Merges dynamic pages with the hardcoded sitemap.
11
+ * - In 'Live Mode' (Builder): REPLACES the sitemap entirely to show only the user's design.
12
+ */
13
+ export const useDynamicSitemap = (staticSitemap: SitemapEntry[]) => {
14
+ const manifest = useManifest();
15
+ const status = useBridgeStatus();
16
+ const isLive = status === 'live'; // ✅ Detect Live Mode
17
+
18
+ return useMemo(() => {
19
+ // 1. Convert Manifest Pages -> Sitemap Entries
20
+ const dynamicEntries: SitemapEntry[] = (manifest.pages || []).map((page) => ({
21
+ id: page.id,
22
+ path: page.slug,
23
+ title: page.title,
24
+ icon: (page.icon as IconName) || 'layout',
25
+ component: () => <DynamicPage pageId={page.id} />
26
+ }));
27
+
28
+ // 2. ✅ OPTIMIZATION: Exclusive Mode
29
+ // If we are connected to the Builder (Live) AND have dynamic content,
30
+ // we drop the static "Showcase" pages.
31
+ // This makes the Kernel feel like a blank slate to the user.
32
+ if (isLive && dynamicEntries.length > 0) {
33
+ // console.log("🚀 [Sitemap] Live Mode Active: Hiding static pages.");
34
+ return dynamicEntries;
35
+ }
36
+
37
+ // 3. Static/Hybrid Mode (Keep existing logic for local dev)
38
+ const staticIds = new Set(staticSitemap.map(s => s.id));
39
+ const uniqueDynamic = dynamicEntries.filter(d => !staticIds.has(d.id));
40
+
41
+ return [...staticSitemap, ...uniqueDynamic];
42
+ }, [manifest, staticSitemap, isLive]);
43
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @file useJustInTimeSeeder.ts
3
+ * @description Runtime Data Generator for the Preview Engine.
4
+ */
5
+
6
+ import { useMemo } from 'react';
7
+ import { getMockData } from '../../data/mockData';
8
+ import type { ResourceDefinition, FieldDefinition } from '../validation/schema';
9
+
10
+ // Helper: Generate a semi-realistic value based on field type
11
+ const generateValue = (field: FieldDefinition, index: number) => {
12
+ const i = index + 1;
13
+ const label = field.label || 'Item';
14
+
15
+ switch (field.type) {
16
+ case 'text':
17
+ return `${label} ${i}`;
18
+ case 'number':
19
+ return Math.floor(Math.random() * 100) + 1;
20
+ case 'currency':
21
+ return (Math.random() * 1000).toFixed(2);
22
+ case 'boolean':
23
+ return Math.random() > 0.5;
24
+ case 'date':
25
+ // Return a date within the last 30 days
26
+ const d = new Date();
27
+ d.setDate(d.getDate() - (i * 2));
28
+ return d.toISOString();
29
+ case 'status':
30
+ return ['Active', 'Pending', 'Inactive', 'Archived'][index % 4];
31
+ case 'email':
32
+ return `user${i}@example.com`;
33
+ case 'image':
34
+ return `https://i.pravatar.cc/150?u=${i}`;
35
+ case 'textarea':
36
+ return `This is a sample description for ${label} ${i}. It contains enough text to test multi-line rendering.`;
37
+ default:
38
+ return `${label}-${i}`;
39
+ }
40
+ };
41
+
42
+ export const useJustInTimeSeeder = (dataId: string, resourceDef?: ResourceDefinition | null) => {
43
+ return useMemo(() => {
44
+ // 1. Priority: Static Data (The "Golden Copy")
45
+ // If the developer has manually added data to mockData.ts, always use that.
46
+ const staticData = getMockData(dataId);
47
+ if (staticData && staticData.length > 0) {
48
+ return staticData;
49
+ }
50
+
51
+ // 2. Fallback: JIT Generation (The "Safety Net")
52
+ // If we have a schema but no data, generate it now.
53
+ if (resourceDef) {
54
+ console.log(`🌱 [JIT Seeder] Generating 5 mock records for: ${dataId}`);
55
+
56
+ return Array.from({ length: 5 }).map((_, idx) => {
57
+ const row: Record<string, any> = {
58
+ // Generate a robust ID that won't collide with future real IDs
59
+ id: `jit_${dataId}_${idx + 1}`
60
+ };
61
+
62
+ resourceDef.fields.forEach(field => {
63
+ // Don't overwrite ID if it was part of the fields list
64
+ if (field.key !== 'id') {
65
+ row[field.key] = generateValue(field, idx);
66
+ }
67
+ });
68
+
69
+ return row;
70
+ });
71
+ }
72
+
73
+ // 3. Empty State (If no schema is found)
74
+ return [];
75
+ }, [dataId, resourceDef]);
76
+ };