@ramme-io/kernel 1.3.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.
Files changed (124) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/LICENSE +21 -0
  3. package/dist/components/AutoForm.d.ts +24 -0
  4. package/dist/components/AutoForm.d.ts.map +1 -0
  5. package/dist/components/AutoForm.js +78 -0
  6. package/dist/components/AutoForm.js.map +1 -0
  7. package/dist/components/SmartTable.d.ts +14 -0
  8. package/dist/components/SmartTable.d.ts.map +1 -0
  9. package/dist/components/SmartTable.js +128 -0
  10. package/dist/components/SmartTable.js.map +1 -0
  11. package/dist/config/app.manifest.d.ts +7 -0
  12. package/dist/config/app.manifest.d.ts.map +1 -0
  13. package/dist/config/app.manifest.js +8 -0
  14. package/dist/config/app.manifest.js.map +1 -0
  15. package/dist/engine/renderers/DynamicBlock.d.ts +9 -0
  16. package/dist/engine/renderers/DynamicBlock.d.ts.map +1 -0
  17. package/dist/engine/renderers/DynamicBlock.js +67 -0
  18. package/dist/engine/renderers/DynamicBlock.js.map +1 -0
  19. package/dist/engine/renderers/DynamicPage.d.ts +11 -0
  20. package/dist/engine/renderers/DynamicPage.d.ts.map +1 -0
  21. package/dist/engine/renderers/DynamicPage.js +91 -0
  22. package/dist/engine/renderers/DynamicPage.js.map +1 -0
  23. package/dist/engine/renderers/route-generator.d.ts +16 -0
  24. package/dist/engine/renderers/route-generator.d.ts.map +1 -0
  25. package/dist/engine/renderers/route-generator.js +30 -0
  26. package/dist/engine/renderers/route-generator.js.map +1 -0
  27. package/dist/engine/renderers/sitemap-entry.d.ts +7 -0
  28. package/dist/engine/renderers/sitemap-entry.d.ts.map +1 -0
  29. package/dist/engine/renderers/sitemap-entry.js +2 -0
  30. package/dist/engine/renderers/sitemap-entry.js.map +1 -0
  31. package/dist/engine/runtime/ManifestContext.d.ts +115 -0
  32. package/dist/engine/runtime/ManifestContext.d.ts.map +1 -0
  33. package/dist/engine/runtime/ManifestContext.js +56 -0
  34. package/dist/engine/runtime/ManifestContext.js.map +1 -0
  35. package/dist/engine/runtime/MqttContext.d.ts +14 -0
  36. package/dist/engine/runtime/MqttContext.d.ts.map +1 -0
  37. package/dist/engine/runtime/MqttContext.js +70 -0
  38. package/dist/engine/runtime/MqttContext.js.map +1 -0
  39. package/dist/engine/runtime/SitemapContext.d.ts +31 -0
  40. package/dist/engine/runtime/SitemapContext.d.ts.map +1 -0
  41. package/dist/engine/runtime/SitemapContext.js +54 -0
  42. package/dist/engine/runtime/SitemapContext.js.map +1 -0
  43. package/dist/engine/runtime/data-seeder.d.ts +10 -0
  44. package/dist/engine/runtime/data-seeder.d.ts.map +1 -0
  45. package/dist/engine/runtime/data-seeder.js +35 -0
  46. package/dist/engine/runtime/data-seeder.js.map +1 -0
  47. package/dist/engine/runtime/useAction.d.ts +4 -0
  48. package/dist/engine/runtime/useAction.d.ts.map +1 -0
  49. package/dist/engine/runtime/useAction.js +55 -0
  50. package/dist/engine/runtime/useAction.js.map +1 -0
  51. package/dist/engine/runtime/useCrudLocalStorage.d.ts +19 -0
  52. package/dist/engine/runtime/useCrudLocalStorage.d.ts.map +1 -0
  53. package/dist/engine/runtime/useCrudLocalStorage.js +73 -0
  54. package/dist/engine/runtime/useCrudLocalStorage.js.map +1 -0
  55. package/dist/engine/runtime/useDataQuery.d.ts +39 -0
  56. package/dist/engine/runtime/useDataQuery.d.ts.map +1 -0
  57. package/dist/engine/runtime/useDataQuery.js +50 -0
  58. package/dist/engine/runtime/useDataQuery.js.map +1 -0
  59. package/dist/engine/runtime/useDynamicSitemap.d.ts +9 -0
  60. package/dist/engine/runtime/useDynamicSitemap.d.ts.map +1 -0
  61. package/dist/engine/runtime/useDynamicSitemap.js +38 -0
  62. package/dist/engine/runtime/useDynamicSitemap.js.map +1 -0
  63. package/dist/engine/runtime/useJustInTimeSeeder.d.ts +11 -0
  64. package/dist/engine/runtime/useJustInTimeSeeder.d.ts.map +1 -0
  65. package/dist/engine/runtime/useJustInTimeSeeder.js +88 -0
  66. package/dist/engine/runtime/useJustInTimeSeeder.js.map +1 -0
  67. package/dist/engine/runtime/useLiveBridge.d.ts +109 -0
  68. package/dist/engine/runtime/useLiveBridge.d.ts.map +1 -0
  69. package/dist/engine/runtime/useLiveBridge.js +21 -0
  70. package/dist/engine/runtime/useLiveBridge.js.map +1 -0
  71. package/dist/engine/runtime/useSignal.d.ts +11 -0
  72. package/dist/engine/runtime/useSignal.d.ts.map +1 -0
  73. package/dist/engine/runtime/useSignal.js +26 -0
  74. package/dist/engine/runtime/useSignal.js.map +1 -0
  75. package/dist/engine/runtime/useSignalStore.d.ts +31 -0
  76. package/dist/engine/runtime/useSignalStore.d.ts.map +1 -0
  77. package/dist/engine/runtime/useSignalStore.js +60 -0
  78. package/dist/engine/runtime/useSignalStore.js.map +1 -0
  79. package/dist/engine/runtime/useWorkflowEngine.d.ts +4 -0
  80. package/dist/engine/runtime/useWorkflowEngine.d.ts.map +1 -0
  81. package/dist/engine/runtime/useWorkflowEngine.js +85 -0
  82. package/dist/engine/runtime/useWorkflowEngine.js.map +1 -0
  83. package/dist/engine/types/manifest-types.d.ts +38 -0
  84. package/dist/engine/types/manifest-types.d.ts.map +1 -0
  85. package/dist/engine/types/manifest-types.js +5 -0
  86. package/dist/engine/types/manifest-types.js.map +1 -0
  87. package/dist/engine/types/sitemap-entry.d.ts +58 -0
  88. package/dist/engine/types/sitemap-entry.d.ts.map +1 -0
  89. package/dist/engine/types/sitemap-entry.js +19 -0
  90. package/dist/engine/types/sitemap-entry.js.map +1 -0
  91. package/dist/engine/validation/schema.d.ts +383 -0
  92. package/dist/engine/validation/schema.d.ts.map +1 -0
  93. package/dist/engine/validation/schema.js +156 -0
  94. package/dist/engine/validation/schema.js.map +1 -0
  95. package/dist/index.d.ts +22 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +24 -0
  98. package/dist/index.js.map +1 -0
  99. package/package.json +36 -0
  100. package/src/components/AutoForm.tsx +141 -0
  101. package/src/components/SmartTable.tsx +316 -0
  102. package/src/config/app.manifest.ts +7 -0
  103. package/src/engine/renderers/DynamicBlock.tsx +84 -0
  104. package/src/engine/renderers/DynamicPage.tsx +196 -0
  105. package/src/engine/renderers/route-generator.tsx +47 -0
  106. package/src/engine/renderers/sitemap-entry.ts +6 -0
  107. package/src/engine/runtime/ManifestContext.tsx +81 -0
  108. package/src/engine/runtime/MqttContext.tsx +94 -0
  109. package/src/engine/runtime/SitemapContext.tsx +61 -0
  110. package/src/engine/runtime/data-seeder.ts +39 -0
  111. package/src/engine/runtime/useAction.ts +64 -0
  112. package/src/engine/runtime/useCrudLocalStorage.ts +82 -0
  113. package/src/engine/runtime/useDataQuery.ts +98 -0
  114. package/src/engine/runtime/useDynamicSitemap.tsx +43 -0
  115. package/src/engine/runtime/useJustInTimeSeeder.ts +101 -0
  116. package/src/engine/runtime/useLiveBridge.ts +24 -0
  117. package/src/engine/runtime/useSignal.ts +40 -0
  118. package/src/engine/runtime/useSignalStore.ts +94 -0
  119. package/src/engine/runtime/useWorkflowEngine.ts +89 -0
  120. package/src/engine/types/manifest-types.ts +45 -0
  121. package/src/engine/types/sitemap-entry.ts +66 -0
  122. package/src/engine/validation/schema.ts +189 -0
  123. package/src/index.ts +27 -0
  124. package/tsconfig.json +28 -0
@@ -0,0 +1,81 @@
1
+ import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
2
+ // ✅ FIX 2: Correct Path to Types
3
+ import type { AppSpecification } from '../validation/schema';
4
+
5
+ interface ManifestContextType {
6
+ manifest: AppSpecification;
7
+ isLive: boolean;
8
+ }
9
+
10
+ const ManifestContext = createContext<ManifestContextType | null>(null);
11
+
12
+ interface ManifestProviderProps {
13
+ children: React.ReactNode;
14
+ manifest: AppSpecification;
15
+ }
16
+
17
+ export const ManifestProvider: React.FC<ManifestProviderProps> = ({ children, manifest: initialManifest }) => {
18
+ // 1. Initialize State
19
+ const [manifest, setManifest] = useState<AppSpecification>(initialManifest);
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) {
72
+ throw new Error('useManifest must be used within a ManifestProvider');
73
+ }
74
+ return context.manifest;
75
+ };
76
+
77
+ // Returns 'live' | 'static' string to match your UI components
78
+ export const useBridgeStatus = () => {
79
+ const context = useContext(ManifestContext);
80
+ return context?.isLive ? 'live' : 'static';
81
+ };
@@ -0,0 +1,94 @@
1
+ import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
2
+ import mqtt, { type MqttClient } from 'mqtt';
3
+ // ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
4
+ // ✅ ADDED: Live Context
5
+ import { useManifest } from './ManifestContext';
6
+
7
+ interface MqttContextType {
8
+ isConnected: boolean;
9
+ lastMessage: Record<string, string>;
10
+ publish: (topic: string, message: string) => void;
11
+ subscribe: (topic: string) => void;
12
+ unsubscribe: (topic: string) => void;
13
+ }
14
+
15
+ const MqttContext = createContext<MqttContextType | null>(null);
16
+
17
+ export const MqttProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
18
+ const [isConnected, setIsConnected] = useState(false);
19
+ const [lastMessage, setLastMessage] = useState<Record<string, string>>({});
20
+ const clientRef = useRef<MqttClient | null>(null);
21
+ const subscriptions = useRef<Set<string>>(new Set());
22
+
23
+ // ✅ 1. Consume Live Manifest
24
+ const appManifest = useManifest();
25
+
26
+ useEffect(() => {
27
+ // ✅ 2. Hot-Swap Broker Connection
28
+ // If you change the Broker URL in the Builder, this effect will re-run!
29
+ const brokerUrl = appManifest.config.brokerUrl || 'wss://test.mosquitto.org:8081';
30
+ console.log(`[MQTT] Connecting to ${brokerUrl}...`);
31
+
32
+ // Disconnect previous if exists
33
+ if (clientRef.current) {
34
+ clientRef.current.end();
35
+ }
36
+
37
+ const client = mqtt.connect(brokerUrl);
38
+ clientRef.current = client;
39
+
40
+ client.on('connect', () => {
41
+ console.log('[MQTT] Connected ✅');
42
+ setIsConnected(true);
43
+ // Re-subscribe to previous topics if needed
44
+ subscriptions.current.forEach(t => client.subscribe(t));
45
+ });
46
+
47
+ client.on('message', (topic: string, payload: Buffer) => {
48
+ const messageStr = payload.toString();
49
+ setLastMessage((prev) => ({ ...prev, [topic]: messageStr }));
50
+ });
51
+
52
+ client.on('error', (err) => {
53
+ console.error('[MQTT] Connection error: ', err);
54
+ client.end();
55
+ });
56
+
57
+ return () => {
58
+ console.log('[MQTT] Disconnecting...');
59
+ client.end();
60
+ };
61
+ }, [appManifest.config.brokerUrl]); // Only re-connect if URL changes
62
+
63
+ const subscribe = (topic: string) => {
64
+ if (clientRef.current && !subscriptions.current.has(topic)) {
65
+ clientRef.current.subscribe(topic);
66
+ subscriptions.current.add(topic);
67
+ }
68
+ };
69
+
70
+ const unsubscribe = (topic: string) => {
71
+ if (clientRef.current && subscriptions.current.has(topic)) {
72
+ clientRef.current.unsubscribe(topic);
73
+ subscriptions.current.delete(topic);
74
+ }
75
+ };
76
+
77
+ const publish = (topic: string, message: string) => {
78
+ if (clientRef.current) {
79
+ clientRef.current.publish(topic, message);
80
+ }
81
+ };
82
+
83
+ return (
84
+ <MqttContext.Provider value={{ isConnected, lastMessage, subscribe, unsubscribe, publish }}>
85
+ {children}
86
+ </MqttContext.Provider>
87
+ );
88
+ };
89
+
90
+ export const useMqtt = () => {
91
+ const context = useContext(MqttContext);
92
+ if (!context) throw new Error('useMqtt must be used within an MqttProvider');
93
+ return context;
94
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @file SitemapContext.tsx
3
+ * @repository ramme-app-starter
4
+ * @description This file implements a React Context for the sitemap.
5
+ *
6
+ * STRATEGIC IMPORTANCE:
7
+ * This context is the "plumbing" that makes our swappable layout
8
+ * strategy possible. It allows a top-level layout component (like
9
+ * `DashboardLayout.tsx` or `DocsLayout.tsx`) to "provide" its specific
10
+ * sitemap array (e.g., `dashboard.sitemap.ts`) to all of its children.
11
+ *
12
+ * Any component deep in the tree (like a sidebar, breadcrumbs, or page
13
+ * header) can then "consume" this sitemap data using the `useSitemap`
14
+ * hook to render the correct navigation, titles, and icons.
15
+ *
16
+ * This effectively decouples the navigation UI from the sitemap data,
17
+ * allowing us to change the entire application's IA simply by changing
18
+ * the data provided by the `SitemapProvider`.
19
+ */
20
+
21
+ import { createContext, useContext } from 'react';
22
+ // Import the canonical "schema" for a sitemap entry
23
+ import type { SitemapEntry } from '../types/sitemap-entry';
24
+
25
+ // 1. Create the context
26
+ // We initialize it with an empty array as a safe default.
27
+ // This context will hold the sitemap array specific to the active layout.
28
+ const SitemapContext = createContext<SitemapEntry[]>([]);
29
+
30
+ /**
31
+ * A custom hook to easily access the active sitemap array.
32
+ * This abstracts the `useContext` logic and provides a clean API
33
+ * for components that need to read the sitemap.
34
+ *
35
+ * @returns {SitemapEntry[]} The sitemap array provided by the nearest
36
+ * `SitemapProvider`.
37
+ */
38
+ export const useSitemap = () => {
39
+ const context = useContext(SitemapContext);
40
+
41
+ // This is a critical developer-friendly check.
42
+ // If a component tries to call `useSitemap` outside of a layout
43
+ // that provides the context, we throw an explicit error.
44
+ if (context === undefined) {
45
+ throw new Error('useSitemap must be used within a SitemapProvider');
46
+ }
47
+
48
+ return context;
49
+ };
50
+
51
+ // 3. Export the Provider
52
+ // We export the Provider component directly. This is what our layout
53
+ // components will use to wrap their content and supply the sitemap data.
54
+ //
55
+ // Example Usage (in a layout file):
56
+ // <SitemapProvider value={dashboardSitemap}>
57
+ // <AppHeader />
58
+ // <AppSidebar />
59
+ // <Outlet />
60
+ // </SitemapProvider>
61
+ export const SitemapProvider = SitemapContext.Provider;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file data-seeder.ts
3
+ * @description The "Big Bang" for the client-side database.
4
+ * * ARCHITECTURAL ROLE:
5
+ * This utility ensures that the LocalStorage "Data Lake" is never empty.
6
+ * On app launch, it checks if data exists. If not, it writes the seed data
7
+ * provided by the Kernel/Manifest into the browser's storage.
8
+ */
9
+
10
+ // 🔒 CRITICAL FIX: Changed from 'ramme_mock_' to 'ramme_db_' to match AuthContext
11
+ // This ensures that the AuthProvider can actually find the 'users' table we seed.
12
+ const DB_PREFIX = 'ramme_db_';
13
+
14
+ export const initializeDataLake = (registry: Record<string, any[]>) => {
15
+ if (typeof window === 'undefined') return;
16
+
17
+ // Safety check: Don't crash if the registry is missing (e.g. during early boot)
18
+ if (!registry) {
19
+ console.warn('⚠️ [Data Lake] No registry provided to seeder. Skipping initialization.');
20
+ return;
21
+ }
22
+
23
+ console.groupCollapsed('🌊 [Data Lake] Initialization');
24
+
25
+ Object.entries(registry).forEach(([key, seedData]) => {
26
+ const storageKey = `${DB_PREFIX}${key}`;
27
+ const existing = localStorage.getItem(storageKey);
28
+
29
+ if (!existing) {
30
+ console.log(`✨ Seeding collection: ${key}`);
31
+ localStorage.setItem(storageKey, JSON.stringify(seedData));
32
+ } else {
33
+ // Optional: Debug log for existing data (helps verify persistence)
34
+ // console.log(`✅ Found existing data for: ${key}`);
35
+ }
36
+ });
37
+
38
+ console.groupEnd();
39
+ };
@@ -0,0 +1,64 @@
1
+ import { useCallback } from 'react';
2
+ import { useMqtt } from './MqttContext';
3
+ // ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
4
+ // ✅ ADDED: Live Context
5
+ import { useManifest } from './ManifestContext';
6
+
7
+ export const useAction = () => {
8
+ const { publish, isConnected } = useMqtt();
9
+
10
+ // ✅ 1. Consume Live Manifest
11
+ const appManifest = useManifest();
12
+ const { config, domain } = appManifest;
13
+
14
+ const sendAction = useCallback(async (entityId: string, value: any) => {
15
+ // 2. Find the Entity definition (Live)
16
+ const entity = domain.entities.find(e => e.id === entityId);
17
+
18
+ if (!entity) {
19
+ console.warn(`[Action] Entity ID '${entityId}' not found in manifest.`);
20
+ return;
21
+ }
22
+
23
+ const signalId = entity.signals[0];
24
+ const signal = domain.signals.find(s => s.id === signalId);
25
+
26
+ if (!signal) {
27
+ console.warn(`[Action] No signal linked to entity: ${entityId}`);
28
+ return;
29
+ }
30
+
31
+ // --- Mock Mode ---
32
+ if (config.mockMode) {
33
+ console.log(`%c[Mock Action] Setting ${entity.name} to:`, 'color: #10b981; font-weight: bold;', value);
34
+ return;
35
+ }
36
+
37
+ // --- Live Mode (MQTT) ---
38
+ if (signal.source === 'mqtt' && signal.topic) {
39
+ if (!isConnected) {
40
+ console.warn('[Action] Cannot send: MQTT disconnected');
41
+ return;
42
+ }
43
+ const payload = typeof value === 'object' ? JSON.stringify(value) : String(value);
44
+ console.log(`[MQTT] Publishing to '${signal.topic}': ${payload}`);
45
+ publish(signal.topic, payload);
46
+ }
47
+
48
+ // --- Live Mode (HTTP) ---
49
+ if (signal.source === 'http' && signal.endpoint) {
50
+ try {
51
+ await fetch(signal.endpoint, {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify({ id: signal.id, value })
55
+ });
56
+ } catch (err) {
57
+ console.log('[HTTP] (Simulation) Request sent.');
58
+ }
59
+ }
60
+
61
+ }, [config.mockMode, isConnected, publish, domain]);
62
+
63
+ return { sendAction };
64
+ };
@@ -0,0 +1,82 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ /**
4
+ * @hook useCrudLocalStorage
5
+ * @description A generic hook for performing CRUD (Create, Read, Update, Delete)
6
+ * operations on an array of items stored in the browser's localStorage.
7
+ * * It serves as the "Database Engine" for the mock runtime.
8
+ *
9
+ * @param storageKey The unique key for this data in localStorage (e.g., 'ramme_db_users').
10
+ * @param initialData The default data to seed localStorage with if it's empty.
11
+ * @returns An object with the current data and functions to manipulate it.
12
+ */
13
+ export const useCrudLocalStorage = <T extends { id: any }>(
14
+ storageKey: string,
15
+ initialData: T[]
16
+ ) => {
17
+ // 1. Initialize State from LocalStorage
18
+ const [data, setData] = useState<T[]>(() => {
19
+ // Safety check for Server-Side Rendering
20
+ if (typeof window === 'undefined') return initialData;
21
+
22
+ try {
23
+ const item = window.localStorage.getItem(storageKey);
24
+ if (item) {
25
+ return JSON.parse(item);
26
+ } else {
27
+ // Seed the "DB" if empty
28
+ window.localStorage.setItem(storageKey, JSON.stringify(initialData));
29
+ return initialData;
30
+ }
31
+ } catch (error) {
32
+ console.error(`[Data Lake] Error reading key "${storageKey}":`, error);
33
+ return initialData;
34
+ }
35
+ });
36
+
37
+ // 2. CREATE
38
+ const createItem = useCallback((newItem: Omit<T, 'id'>) => {
39
+ setData(prevData => {
40
+ // ✅ ROBUST ID GENERATION (Zero Jank)
41
+ // Detects if existing IDs are Numbers or Strings to prevent type conflicts.
42
+ let newId: any;
43
+ const isNumeric = prevData.length > 0 && typeof prevData[0].id === 'number';
44
+
45
+ if (isNumeric) {
46
+ const maxId = prevData.reduce((max, item) => (typeof item.id === 'number' && item.id > max ? item.id : max), 0);
47
+ newId = maxId + 1;
48
+ } else {
49
+ // Fallback for string IDs (e.g. 'usr_17354...')
50
+ newId = `id_${Date.now()}`;
51
+ }
52
+
53
+ const fullNewItem = { ...newItem, id: newId } as T;
54
+
55
+ const updatedData = [...prevData, fullNewItem];
56
+ window.localStorage.setItem(storageKey, JSON.stringify(updatedData));
57
+ return updatedData;
58
+ });
59
+ }, [storageKey]);
60
+
61
+ // 3. UPDATE
62
+ const updateItem = useCallback((updatedItem: T) => {
63
+ setData(prevData => {
64
+ const updatedData = prevData.map(item =>
65
+ item.id === updatedItem.id ? updatedItem : item
66
+ );
67
+ window.localStorage.setItem(storageKey, JSON.stringify(updatedData));
68
+ return updatedData;
69
+ });
70
+ }, [storageKey]);
71
+
72
+ // 4. DELETE
73
+ const deleteItem = useCallback((id: T['id']) => {
74
+ setData(prevData => {
75
+ const updatedData = prevData.filter(item => item.id !== id);
76
+ window.localStorage.setItem(storageKey, JSON.stringify(updatedData));
77
+ return updatedData;
78
+ });
79
+ }, [storageKey]);
80
+
81
+ return { data, createItem, updateItem, deleteItem };
82
+ };
@@ -0,0 +1,98 @@
1
+ import { useMemo } from 'react';
2
+
3
+ /**
4
+ * @file useDataQuery.ts
5
+ * @description The "In-Memory Database Engine".
6
+ *
7
+ * ARCHITECTURAL ROLE:
8
+ * Since this application runs without a real backend, this hook acts as the
9
+ * SQL Query Engine. It takes raw arrays from the Data Lake and performs
10
+ * real-time filtering, sorting, and pagination before passing the result
11
+ * to the UI.
12
+ *
13
+ * CAPABILITIES:
14
+ * 1. WHERE: Supports complex filtering (equals, contains, gt, lt).
15
+ * 2. ORDER BY: Handles ascending/descending sorts on any field.
16
+ * 3. LIMIT/OFFSET: Calculates pagination slices automatically.
17
+ */
18
+
19
+ export type SortDirection = 'asc' | 'desc';
20
+
21
+ export interface SortOption {
22
+ field: string;
23
+ direction: SortDirection;
24
+ }
25
+
26
+ export interface FilterOption {
27
+ field: string;
28
+ operator: 'equals' | 'contains' | 'gt' | 'lt' | 'neq';
29
+ value: any;
30
+ }
31
+
32
+ export interface QueryOptions {
33
+ filters?: FilterOption[];
34
+ sort?: SortOption;
35
+ page?: number;
36
+ pageSize?: number;
37
+ }
38
+
39
+ export interface QueryResult<T> {
40
+ data: T[];
41
+ total: number;
42
+ pageCount: number;
43
+ }
44
+
45
+ // --- 2. Update the Hook Signature (Fixes ts(2345)) ---
46
+
47
+ export function useDataQuery<T>(
48
+ rawData: T[], // <--- Now accepts an Array, NOT a string ID
49
+ options: QueryOptions = {}
50
+ ): QueryResult<T> {
51
+ const { filters, sort, page = 1, pageSize = 10 } = options;
52
+
53
+ // A. Filtering Logic
54
+ const filteredData = useMemo(() => {
55
+ if (!filters || filters.length === 0) return rawData;
56
+
57
+ return rawData.filter((item: any) => {
58
+ return filters.every((filter) => {
59
+ const itemValue = item[filter.field];
60
+
61
+ switch (filter.operator) {
62
+ case 'equals': return itemValue == filter.value;
63
+ case 'neq': return itemValue != filter.value;
64
+ case 'contains':
65
+ return String(itemValue).toLowerCase().includes(String(filter.value).toLowerCase());
66
+ case 'gt': return itemValue > filter.value;
67
+ case 'lt': return itemValue < filter.value;
68
+ default: return true;
69
+ }
70
+ });
71
+ });
72
+ }, [rawData, filters]);
73
+
74
+ // B. Sorting Logic
75
+ const sortedData = useMemo(() => {
76
+ if (!sort) return filteredData;
77
+
78
+ return [...filteredData].sort((a: any, b: any) => {
79
+ const aValue = a[sort.field];
80
+ const bValue = b[sort.field];
81
+ if (aValue < bValue) return sort.direction === 'asc' ? -1 : 1;
82
+ if (aValue > bValue) return sort.direction === 'asc' ? 1 : -1;
83
+ return 0;
84
+ });
85
+ }, [filteredData, sort]);
86
+
87
+ // C. Pagination Logic
88
+ const paginatedResult = useMemo(() => {
89
+ const startIndex = (page - 1) * pageSize;
90
+ return sortedData.slice(startIndex, startIndex + pageSize);
91
+ }, [sortedData, page, pageSize]);
92
+
93
+ return {
94
+ data: paginatedResult,
95
+ total: filteredData.length,
96
+ pageCount: Math.ceil(filteredData.length / pageSize),
97
+ };
98
+ }
@@ -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,101 @@
1
+ /**
2
+ * @file useJustInTimeSeeder.ts
3
+ * @description Runtime Data Generator for the Preview Engine.
4
+ * * ARCHITECTURAL NOTE:
5
+ * This hook is "Storage Aware." It does not import static files.
6
+ * It checks the 'ramme_db_' keys in localStorage (which were seeded by main.tsx).
7
+ * If data exists, it uses it. If not, it procedurally generates it.
8
+ */
9
+
10
+ import { useMemo } from 'react';
11
+
12
+ import type { ResourceDefinition, FieldDefinition } from '../validation/schema';
13
+
14
+ // Match the prefix defined in data-seeder.ts
15
+ const DB_PREFIX = 'ramme_db_';
16
+
17
+ // Helper: Generate a semi-realistic value based on field type
18
+ const generateValue = (field: FieldDefinition, index: number) => {
19
+ const i = index + 1;
20
+ const label = field.label || 'Item';
21
+
22
+ switch (field.type) {
23
+ case 'text':
24
+ return `${label} ${i}`;
25
+ case 'number':
26
+ return Math.floor(Math.random() * 100) + 1;
27
+ case 'currency':
28
+ return (Math.random() * 1000).toFixed(2);
29
+ case 'boolean':
30
+ return Math.random() > 0.5;
31
+ case 'date':
32
+ const d = new Date();
33
+ d.setDate(d.getDate() - (i * 2));
34
+ return d.toISOString().split('T')[0]; // Simple YYYY-MM-DD
35
+ case 'status':
36
+ return ['Active', 'Pending', 'Inactive', 'Archived'][index % 4];
37
+ case 'email':
38
+ return `user${i}@example.com`;
39
+ case 'image':
40
+ return `https://i.pravatar.cc/150?u=${i}`;
41
+ case 'textarea':
42
+ return `This is a sample description for ${label} ${i}. It contains enough text to test multi-line rendering.`;
43
+ default:
44
+ return `${label}-${i}`;
45
+ }
46
+ };
47
+
48
+ export const useJustInTimeSeeder = (dataId: string, resourceDef?: ResourceDefinition | null) => {
49
+ return useMemo(() => {
50
+ if (!dataId) return [];
51
+
52
+ // 1. Priority: The Data Lake (Static Seed + User Edits)
53
+ // We trust that main.tsx has already seeded the "Golden Copy" from mockData.ts
54
+ // into localStorage. This ensures we see the EXACT same data as the rest of the app.
55
+ if (typeof window !== 'undefined') {
56
+ const storageKey = `${DB_PREFIX}${dataId}`;
57
+ const localData = localStorage.getItem(storageKey);
58
+
59
+ if (localData) {
60
+ try {
61
+ const parsed = JSON.parse(localData);
62
+ // Only return if we actually have rows
63
+ if (Array.isArray(parsed) && parsed.length > 0) {
64
+ return parsed;
65
+ }
66
+ } catch (e) {
67
+ console.warn(`[JIT Seeder] Corrupt data in ${storageKey}, falling back to generation.`);
68
+ }
69
+ }
70
+ }
71
+
72
+ // 2. Fallback: JIT Generation (The "Safety Net")
73
+ // If we have a schema but absolutely zero data in storage, generate it now.
74
+ if (resourceDef) {
75
+ console.log(`🌱 [JIT Seeder] Generating 5 mock records for: ${dataId}`);
76
+
77
+ const generatedData = Array.from({ length: 5 }).map((_, idx) => {
78
+ const row: Record<string, any> = {
79
+ // Generate a robust ID that won't collide with future real IDs
80
+ id: `jit_${dataId}_${idx + 1}`
81
+ };
82
+
83
+ resourceDef.fields.forEach(field => {
84
+ if (field.key !== 'id') {
85
+ row[field.key] = generateValue(field, idx);
86
+ }
87
+ });
88
+
89
+ return row;
90
+ });
91
+
92
+ // OPTIONAL: Persist this generated data so it doesn't regenerate on reload?
93
+ // localStorage.setItem(`${DB_PREFIX}${dataId}`, JSON.stringify(generatedData));
94
+
95
+ return generatedData;
96
+ }
97
+
98
+ // 3. Empty State (No data, no schema)
99
+ return [];
100
+ }, [dataId, resourceDef]);
101
+ };
@@ -0,0 +1,24 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { AppSpecification } from '../validation/schema';
3
+ import { useMqtt } from './MqttContext';
4
+
5
+ const staticManifest = { name: 'loading', version: '0.0.0', capabilities: [], navigation: [] };
6
+
7
+ export function useLiveBridge() {
8
+ // 👇 FIX: Cast the initial state to satisfy the compiler
9
+ const [manifest, setManifest] = useState<AppSpecification>(staticManifest as unknown as AppSpecification);
10
+ const { lastMessage, isConnected } = useMqtt();
11
+
12
+ useEffect(() => {
13
+ if (lastMessage && lastMessage.topic === 'ramme/manifest/update') {
14
+ try {
15
+ const payload = JSON.parse(lastMessage.payload.toString());
16
+ setManifest(payload);
17
+ } catch (e) {
18
+ console.error('Failed to parse manifest update', e);
19
+ }
20
+ }
21
+ }, [lastMessage]);
22
+
23
+ return { manifest, isLive: isConnected };
24
+ }