@ramme-io/create-app 1.2.1 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -15
- package/template/package.json +41 -0
- package/template/pkg.json +9 -7
- package/template/src/App.tsx +72 -31
- package/template/src/components/AIChatWidget.tsx +2 -2
- package/template/src/components/AppHeader.tsx +12 -12
- package/template/src/components/AutoForm.tsx +13 -0
- package/template/src/{pages/styleguide → components}/NotFound.tsx +1 -1
- package/template/src/components/PageTitleUpdater.tsx +2 -2
- package/template/src/components/ProtectedRoute.tsx +18 -1
- package/template/src/components/ScrollToTop.tsx +19 -0
- package/template/src/components/dashboard/ChartLine.tsx +28 -0
- package/template/src/components/dashboard/StatCard.tsx +61 -0
- package/template/src/config/app.manifest.ts +3 -1
- package/template/src/config/component-registry.tsx +69 -0
- package/template/src/config/navigation.ts +1 -1
- package/template/src/data/mock-charts.ts +32 -28
- package/template/src/{components → engine/renderers}/DynamicBlock.tsx +28 -8
- package/template/src/engine/renderers/DynamicPage.tsx +150 -0
- package/template/src/engine/runtime/ManifestContext.tsx +79 -0
- package/template/src/{contexts → engine/runtime}/MqttContext.tsx +23 -14
- package/template/src/{contexts → engine/runtime}/SitemapContext.tsx +1 -1
- package/template/src/engine/runtime/data-seeder.ts +47 -0
- package/template/src/{hooks → engine/runtime}/useAction.ts +11 -14
- package/template/src/{hooks → engine/runtime}/useCrudLocalStorage.ts +27 -8
- package/template/src/{hooks → engine/runtime}/useDataQuery.ts +15 -1
- package/template/src/engine/runtime/useDynamicSitemap.tsx +43 -0
- package/template/src/engine/runtime/useJustInTimeSeeder.ts +76 -0
- package/template/src/engine/runtime/useLiveBridge.ts +44 -0
- package/template/src/engine/runtime/useSignal.ts +40 -0
- package/template/src/{generated/hooks.ts → engine/runtime/useSignalStore.ts} +35 -8
- package/template/src/engine/runtime/useWorkflowEngine.ts +89 -0
- package/template/src/{core → engine/types}/manifest-types.ts +35 -3
- package/template/src/{types → engine/validation}/schema.ts +17 -0
- package/template/src/{pages → features/ai/pages}/AiChat.tsx +1 -1
- package/template/src/features/auth/AuthContext.tsx +118 -0
- package/template/src/features/auth/pages/AuthLayout.tsx +55 -0
- package/template/src/features/auth/pages/LoginPage.tsx +106 -0
- package/template/src/features/auth/pages/SignupPage.tsx +96 -0
- package/template/src/{blocks → features/datagrid}/SmartTable.tsx +41 -25
- package/template/src/features/developer/GhostOverlay.tsx +114 -0
- package/template/src/{pages → features/onboarding/pages}/Welcome.tsx +0 -1
- package/template/src/features/overview/index.ts +1 -0
- package/template/src/features/overview/pages/OverviewPage.tsx +127 -0
- package/template/src/{pages → features/playground/pages}/AccountingLedgerPage.tsx +1 -1
- package/template/src/{pages/prototypes → features/playground/pages}/ItemSelectorPage.tsx +1 -1
- package/template/src/{pages/settings → features/settings/pages}/BillingPage.tsx +1 -1
- package/template/src/features/settings/pages/ProfilePage.tsx +153 -0
- package/template/src/{pages/settings → features/settings/pages}/TeamPage.tsx +1 -1
- package/template/src/features/styleguide/Styleguide.tsx +75 -0
- package/template/src/features/users/components/UserDrawer.tsx +138 -0
- package/template/src/features/users/index.ts +2 -0
- package/template/src/features/users/pages/UsersPage.tsx +151 -0
- package/template/src/features/visualizations/SmartChart.tsx +178 -0
- package/template/src/index.css +1 -1
- package/template/src/main.tsx +27 -15
- package/template/src/templates/dashboard/DashboardLayout.tsx +77 -107
- package/template/src/templates/dashboard/dashboard.sitemap.ts +19 -22
- package/template/src/templates/docs/DocsLayout.tsx +49 -38
- package/template/src/templates/docs/docs.sitemap.ts +22 -34
- package/template/src/templates/settings/SettingsLayout.tsx +83 -143
- package/template/src/templates/settings/settings.sitemap.ts +6 -6
- package/template/tailwind.config.cjs +10 -9
- package/template/vite.config.ts +0 -11
- package/template/src/adaptors/.gitkeep +0 -0
- package/template/src/components/LocalSideNav.tsx +0 -120
- package/template/src/components/PageWithSideNav.tsx +0 -69
- package/template/src/components/dev/GhostOverlay.tsx +0 -68
- package/template/src/config/dashboard.layout.ts +0 -110
- package/template/src/contexts/AuthContext.tsx +0 -64
- package/template/src/core/component-registry.tsx +0 -56
- package/template/src/core/data-seeder.ts +0 -35
- package/template/src/data/mockUsers.ts +0 -18
- package/template/src/hooks/useSignal.ts +0 -83
- package/template/src/hooks/useWorkflowEngine.ts +0 -123
- package/template/src/layouts/DataLayout.tsx +0 -37
- package/template/src/layouts/SideNavLayout.tsx +0 -28
- package/template/src/pages/Dashboard.tsx +0 -60
- package/template/src/pages/DataGridPage.tsx +0 -184
- package/template/src/pages/DynamicPage.tsx +0 -95
- package/template/src/pages/LoginPage.tsx +0 -58
- package/template/src/pages/settings/ProfilePage.tsx +0 -10
- package/template/src/pages/styleguide/Styleguide.tsx +0 -40
- package/template/src/templates/docs/pages/Introduction.tsx +0 -13
- package/template/src/types/signal.ts +0 -23
- /package/template/src/{core → engine/renderers}/route-generator.tsx +0 -0
- /package/template/src/{core → engine/types}/sitemap-entry.ts +0 -0
- /package/template/src/{pages → features}/GenericContentPage.tsx +0 -0
- /package/template/src/{hooks → features/assistant}/useMockChat.ts +0 -0
- /package/template/src/{hooks → features/developer}/useDevTools.ts +0 -0
- /package/template/src/{pages → features}/styleguide/sections/charts/ChartsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/colors/ColorsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/elements/ElementsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/feedback/FeedbackSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/forms/FormsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/icons/IconsSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/layout/LayoutSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/navigation/NavigationSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/tables/TablesSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/templates/TemplatesSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/theming/ThemingSection.tsx +0 -0
- /package/template/src/{pages → features}/styleguide/sections/utilities/UtilitiesSection.tsx +0 -0
|
@@ -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
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useSignalStore } from './useSignalStore';
|
|
3
|
+
// ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
|
|
4
|
+
// ✅ ADDED: Live Context
|
|
5
|
+
import { useManifest } from './ManifestContext';
|
|
6
|
+
|
|
7
|
+
export interface SignalState {
|
|
8
|
+
id: string;
|
|
9
|
+
value: any;
|
|
10
|
+
unit?: string;
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
timestamp?: number;
|
|
14
|
+
status: 'fresh' | 'stale';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useSignal = (signalId: string): SignalState => {
|
|
18
|
+
const signalData = useSignalStore((state) => state.signals[signalId]);
|
|
19
|
+
|
|
20
|
+
// ✅ 1. Consume Live Manifest
|
|
21
|
+
const appManifest = useManifest();
|
|
22
|
+
|
|
23
|
+
// 2. Get Static Definition from Manifest (Live)
|
|
24
|
+
const signalDef = useMemo(() => {
|
|
25
|
+
return appManifest.domain.signals.find((s) => s.id === signalId);
|
|
26
|
+
}, [signalId, appManifest]); // Re-run if manifest updates
|
|
27
|
+
|
|
28
|
+
const value = signalData?.value ?? signalDef?.defaultValue ?? 0;
|
|
29
|
+
const isStale = signalData ? (Date.now() - signalData.timestamp > 10000) : true;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
id: signalId,
|
|
33
|
+
value: value,
|
|
34
|
+
unit: signalDef?.unit,
|
|
35
|
+
min: signalDef?.min,
|
|
36
|
+
max: signalDef?.max,
|
|
37
|
+
timestamp: signalData?.timestamp,
|
|
38
|
+
status: isStale ? 'stale' : 'fresh'
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* @file useSignalStore.ts
|
|
6
|
+
* @description The "Short-Term Memory" of the application.
|
|
7
|
+
*
|
|
8
|
+
* ARCHITECTURAL ROLE:
|
|
9
|
+
* This Zustand store holds the instantaneous value of every Signal (IoT sensor).
|
|
10
|
+
* It acts as the buffer between the high-speed data stream (MQTT/Simulation)
|
|
11
|
+
* and the UI components.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// --- 1. SIGNAL STORE TYPES ---
|
|
5
15
|
export interface SignalValue {
|
|
6
16
|
value: any;
|
|
7
17
|
timestamp: number;
|
|
@@ -9,11 +19,13 @@ export interface SignalValue {
|
|
|
9
19
|
|
|
10
20
|
interface SignalStore {
|
|
11
21
|
signals: Record<string, SignalValue>;
|
|
22
|
+
// Single update (used by UI controls)
|
|
12
23
|
updateSignal: (id: string, value: any) => void;
|
|
24
|
+
// Batch update (used by MQTT/Simulation)
|
|
13
25
|
updateSignals: (updates: Record<string, any>) => void;
|
|
14
26
|
}
|
|
15
27
|
|
|
16
|
-
// Initial State matches
|
|
28
|
+
// Initial State matches your dashboard IDs to prevent "undefined" errors on load
|
|
17
29
|
const initialState = {
|
|
18
30
|
living_room_ac: { value: 72, timestamp: Date.now() },
|
|
19
31
|
living_room_hum: { value: 45, timestamp: Date.now() },
|
|
@@ -40,28 +52,43 @@ export const useSignalStore = create<SignalStore>((set) => ({
|
|
|
40
52
|
})
|
|
41
53
|
}));
|
|
42
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Hook to access specific signals in a component.
|
|
57
|
+
* Usage: const signals = useGeneratedSignals();
|
|
58
|
+
*/
|
|
43
59
|
export const useGeneratedSignals = () => {
|
|
44
60
|
return useSignalStore((state) => state.signals);
|
|
45
61
|
};
|
|
46
62
|
|
|
47
|
-
// --- 2. SIMULATION ENGINE ---
|
|
48
|
-
|
|
63
|
+
// --- 2. SIMULATION ENGINE (The Missing Piece) ---
|
|
64
|
+
/**
|
|
65
|
+
* A hook that generates fake data for testing when no MQTT broker is connected.
|
|
66
|
+
* You can toggle this on/off via the manifest config.
|
|
67
|
+
*/
|
|
68
|
+
export const useSimulation = (isEnabled: boolean = true) => {
|
|
49
69
|
const { updateSignals } = useSignalStore();
|
|
50
70
|
|
|
51
71
|
useEffect(() => {
|
|
52
|
-
|
|
72
|
+
if (!isEnabled) return;
|
|
73
|
+
|
|
74
|
+
console.log("[System] Simulation Mode: ON 🎲");
|
|
53
75
|
const interval = setInterval(() => {
|
|
54
|
-
// Simulate random fluctuations
|
|
76
|
+
// Simulate random fluctuations
|
|
55
77
|
const updates: Record<string, any> = {};
|
|
56
78
|
|
|
57
|
-
// Randomize values slightly
|
|
79
|
+
// Randomize values slightly around a baseline
|
|
58
80
|
updates['living_room_ac'] = Number((72 + (Math.random() * 4 - 2)).toFixed(1));
|
|
59
81
|
updates['living_room_hum'] = Number((45 + (Math.random() * 6 - 3)).toFixed(1));
|
|
60
82
|
updates['server_01'] = Math.floor(Math.random() * 100);
|
|
61
83
|
|
|
84
|
+
// Randomly flip the lock status occasionally (1% chance)
|
|
85
|
+
if (Math.random() > 0.99) {
|
|
86
|
+
updates['front_door_lock'] = Math.random() > 0.5 ? 'LOCKED' : 'UNLOCKED';
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
updateSignals(updates);
|
|
63
90
|
}, 2000); // Update every 2 seconds
|
|
64
91
|
|
|
65
92
|
return () => clearInterval(interval);
|
|
66
|
-
}, [updateSignals]);
|
|
93
|
+
}, [isEnabled, updateSignals]);
|
|
67
94
|
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useToast } from '@ramme-io/ui';
|
|
3
|
+
import { useGeneratedSignals, useSimulation } from './useSignalStore';
|
|
4
|
+
// ❌ REMOVED: import { appManifest } from '../../config/app.manifest';
|
|
5
|
+
// ✅ ADDED: Live Context
|
|
6
|
+
import { useManifest } from './ManifestContext';
|
|
7
|
+
|
|
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[]; }
|
|
11
|
+
|
|
12
|
+
export const useWorkflowEngine = () => {
|
|
13
|
+
const signals = useGeneratedSignals();
|
|
14
|
+
const { addToast } = useToast();
|
|
15
|
+
|
|
16
|
+
// ✅ 1. Consume Live Manifest
|
|
17
|
+
const appManifest = useManifest();
|
|
18
|
+
|
|
19
|
+
// 2. Activate Simulation (Responsive to toggle)
|
|
20
|
+
useSimulation(appManifest.config.mockMode);
|
|
21
|
+
|
|
22
|
+
const executeAction = async (action: ActionDefinition, context: any) => {
|
|
23
|
+
// ... (Same execution logic as before) ...
|
|
24
|
+
console.log(`[Engine] Executing: ${action.type}`, action);
|
|
25
|
+
switch (action.type) {
|
|
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':
|
|
30
|
+
addToast('AI Agent Analyzing...', 'info');
|
|
31
|
+
setTimeout(() => addToast('🤖 Agent: "System Nominal"', 'success', 3000), 1500);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// 3. Watch Signals & Trigger Workflows (Live)
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!appManifest.domain?.workflows) return;
|
|
39
|
+
|
|
40
|
+
(appManifest.domain.workflows as unknown as WorkflowDefinition[]).forEach((flow) => {
|
|
41
|
+
if (!flow.active) return;
|
|
42
|
+
|
|
43
|
+
if (flow.trigger.type === 'signal_change') {
|
|
44
|
+
const signalId = flow.trigger.config.signalId;
|
|
45
|
+
const condition = flow.trigger.config.condition;
|
|
46
|
+
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
const signal = signals[signalId];
|
|
49
|
+
|
|
50
|
+
if (signal) {
|
|
51
|
+
const val = signal.value;
|
|
52
|
+
try {
|
|
53
|
+
const isMet = checkCondition(val, condition);
|
|
54
|
+
if (isMet) {
|
|
55
|
+
console.log(`[Engine] Trigger Fired: ${flow.name}`);
|
|
56
|
+
flow.actions.forEach(action => executeAction(action, { signal: val }));
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}, [signals, addToast, appManifest.domain.workflows]); // Added dependency
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
triggerWorkflow: (workflowId: string) => {
|
|
66
|
+
// ✅ 4. Manual Triggers now find new workflows instantly
|
|
67
|
+
const flow = appManifest.domain?.workflows?.find(w => w.id === workflowId);
|
|
68
|
+
if (flow) {
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
flow.actions.forEach(action => executeAction(action, { manual: true }));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const checkCondition = (value: number, condition: string): boolean => {
|
|
77
|
+
if (!condition) return false;
|
|
78
|
+
const parts = condition.trim().split(' ');
|
|
79
|
+
const operator = parts[0];
|
|
80
|
+
const target = parseFloat(parts[1]);
|
|
81
|
+
switch (operator) {
|
|
82
|
+
case '>': return value > target;
|
|
83
|
+
case '<': return value < target;
|
|
84
|
+
case '>=': return value >= target;
|
|
85
|
+
case '<=': return value <= target;
|
|
86
|
+
case '==': return value === target;
|
|
87
|
+
default: return false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
// src/core/manifest-types.ts
|
|
2
2
|
|
|
3
|
+
import { type IconName } from '@ramme-io/ui';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Defines a simple navigation link structure used in menus.
|
|
7
|
+
*/
|
|
8
|
+
export interface ManifestLink {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
path: string;
|
|
12
|
+
icon?: IconName;
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
/**
|
|
4
16
|
* The fundamental unit of the UI.
|
|
5
17
|
* Corresponds to a specific React component in the registry.
|
|
@@ -40,12 +52,32 @@ export interface PageDefinition {
|
|
|
40
52
|
sections: PageSection[];
|
|
41
53
|
}
|
|
42
54
|
|
|
55
|
+
// ✅ NEW: Full App Specification Types (Matches app.manifest.ts)
|
|
56
|
+
export interface AppMeta {
|
|
57
|
+
name: string;
|
|
58
|
+
version: string;
|
|
59
|
+
description: string;
|
|
60
|
+
author: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AppConfig {
|
|
64
|
+
theme: 'system' | 'light' | 'dark';
|
|
65
|
+
mockMode: boolean;
|
|
66
|
+
brokerUrl?: string; // Added for MQTT
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AppDomain {
|
|
70
|
+
signals: any[];
|
|
71
|
+
entities: any[];
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
/**
|
|
44
75
|
* The "Brain" of the application.
|
|
45
76
|
* This JSON structure drives the entire UI.
|
|
46
77
|
*/
|
|
47
|
-
export interface
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
export interface AppSpecification {
|
|
79
|
+
meta: AppMeta;
|
|
80
|
+
config: AppConfig;
|
|
81
|
+
domain: AppDomain;
|
|
50
82
|
pages: PageDefinition[];
|
|
51
83
|
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @file schema.ts
|
|
5
|
+
* @description The "Application Constitution".
|
|
6
|
+
*
|
|
7
|
+
* ARCHITECTURAL ROLE:
|
|
8
|
+
* This file uses Zod to define the strict runtime validation rules for the
|
|
9
|
+
* App Manifest. While `manifest-types.ts` handles compile-time TypeScript checks,
|
|
10
|
+
* this file handles runtime validation, ensuring that any JSON loaded into the
|
|
11
|
+
* engine (whether from a file, API, or user input) is structurally sound.
|
|
12
|
+
*
|
|
13
|
+
* LAYERS DEFINED:
|
|
14
|
+
* 1. **SaaS Layer:** Data resources, fields, and tables.
|
|
15
|
+
* 2. **Physical Layer:** IoT signals, sensors, and entities.
|
|
16
|
+
* 3. **Logic Layer:** Workflows, triggers, and automated actions.
|
|
17
|
+
* 4. **Presentation Layer:** Pages, sections, and UI blocks.
|
|
18
|
+
*/
|
|
19
|
+
|
|
3
20
|
// ------------------------------------------------------------------
|
|
4
21
|
// 1. DATA RESOURCE DEFINITIONS (SaaS Layer)
|
|
5
22
|
// ------------------------------------------------------------------
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
Message,
|
|
8
8
|
PromptInput,
|
|
9
9
|
} from '@ramme-io/ui';
|
|
10
|
-
import { useMockChat } from '
|
|
10
|
+
import { useMockChat } from '../../assistant/useMockChat'; // <--- The new brain
|
|
11
11
|
|
|
12
12
|
const AiChat: React.FC = () => {
|
|
13
13
|
const { messages, isLoading, sendMessage } = useMockChat();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
// Import the User type from your mock data to ensure shape consistency
|
|
3
|
+
import type { User } from '../../data/mockData';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file AuthContext.tsx
|
|
7
|
+
* @description The Central Identity Manager.
|
|
8
|
+
* * ARCHITECTURAL ROLE:
|
|
9
|
+
* This context provides global access to the current user's state (Identity).
|
|
10
|
+
* It decouples the UI components from the specific authentication implementation.
|
|
11
|
+
* * KEY FEATURES:
|
|
12
|
+
* 1. **Session Persistence:** Checks localStorage on boot.
|
|
13
|
+
* 2. **Data Lake Connection:** Reads from 'ramme_db_users' to validate real accounts.
|
|
14
|
+
* 3. **Simulation:** Adds artificial latency to test loading states.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ✅ CRITICAL FIX: Match the key used by useCrudLocalStorage and data-seeder
|
|
18
|
+
const USER_DB_KEY = 'ramme_db_users';
|
|
19
|
+
const SESSION_KEY = 'ramme_session';
|
|
20
|
+
|
|
21
|
+
interface AuthContextType {
|
|
22
|
+
user: User | null;
|
|
23
|
+
login: (email: string, password: string) => Promise<void>;
|
|
24
|
+
logout: () => void;
|
|
25
|
+
isAuthenticated: boolean;
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
32
|
+
const [user, setUser] = useState<User | null>(null);
|
|
33
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
34
|
+
|
|
35
|
+
// Check for existing session on mount
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const storedUser = localStorage.getItem(SESSION_KEY);
|
|
38
|
+
if (storedUser) {
|
|
39
|
+
try {
|
|
40
|
+
setUser(JSON.parse(storedUser));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error("Failed to parse session", e);
|
|
43
|
+
localStorage.removeItem(SESSION_KEY);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const login = async (email: string, password: string) => {
|
|
50
|
+
setIsLoading(true);
|
|
51
|
+
|
|
52
|
+
// ✅ FIX: We now use the 'password' variable to satisfy TypeScript (ts(6133))
|
|
53
|
+
// This also adds a layer of realism to the mock validation.
|
|
54
|
+
if (!password || password.length < 3) {
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
throw new Error("Password must be at least 3 characters");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`🔐 Attempting login for: ${email}`);
|
|
60
|
+
|
|
61
|
+
// Simulate API Delay
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
63
|
+
|
|
64
|
+
// ✅ READ FROM THE CORRECT DATA LAKE
|
|
65
|
+
const storedUsers = localStorage.getItem(USER_DB_KEY);
|
|
66
|
+
|
|
67
|
+
// Debugging: See what is actually in storage
|
|
68
|
+
if (!storedUsers) {
|
|
69
|
+
console.warn(`⚠️ Data Lake '${USER_DB_KEY}' is empty! Did you run the seeder or signup?`);
|
|
70
|
+
} else {
|
|
71
|
+
// Optional: Log count for debugging (remove in production)
|
|
72
|
+
// console.log(`✅ Data Lake found. Users in DB:`, JSON.parse(storedUsers).length);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const users: User[] = storedUsers ? JSON.parse(storedUsers) : [];
|
|
76
|
+
|
|
77
|
+
// Case-insensitive email check
|
|
78
|
+
const foundUser = users.find(u => u.email.toLowerCase() === email.toLowerCase());
|
|
79
|
+
|
|
80
|
+
if (foundUser) {
|
|
81
|
+
console.log("🎉 User found:", foundUser);
|
|
82
|
+
setUser(foundUser);
|
|
83
|
+
localStorage.setItem(SESSION_KEY, JSON.stringify(foundUser));
|
|
84
|
+
setIsLoading(false);
|
|
85
|
+
} else {
|
|
86
|
+
console.error("❌ User NOT found. Available emails:", users.map(u => u.email));
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
throw new Error("Invalid email or password");
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const logout = () => {
|
|
93
|
+
setUser(null);
|
|
94
|
+
localStorage.removeItem(SESSION_KEY);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<AuthContext.Provider
|
|
99
|
+
value={{
|
|
100
|
+
user,
|
|
101
|
+
login,
|
|
102
|
+
logout,
|
|
103
|
+
isAuthenticated: !!user,
|
|
104
|
+
isLoading
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</AuthContext.Provider>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const useAuth = () => {
|
|
113
|
+
const context = useContext(AuthContext);
|
|
114
|
+
if (context === undefined) {
|
|
115
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
116
|
+
}
|
|
117
|
+
return context;
|
|
118
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Outlet } from 'react-router-dom';
|
|
3
|
+
import { useTheme, Button, availableThemes, type ThemeConfig } from '@ramme-io/ui';
|
|
4
|
+
|
|
5
|
+
// --- Temporary Theme Switcher (Unchanged) ---
|
|
6
|
+
const MiniThemeSwitcher = () => {
|
|
7
|
+
const { setTheme, customTheme, setCustomTheme } = useTheme();
|
|
8
|
+
|
|
9
|
+
const cyberTheme: ThemeConfig = {
|
|
10
|
+
name: 'Cyberpunk',
|
|
11
|
+
colors: {
|
|
12
|
+
primary: '255 238 88', primaryForeground: '0 0 0',
|
|
13
|
+
secondary: '30 30 35', secondaryForeground: '255 255 255',
|
|
14
|
+
accent: '0 255 255', accentForeground: '0 0 0',
|
|
15
|
+
danger: '255 0 85', dangerForeground: '255 255 255',
|
|
16
|
+
background: '5 5 10', card: '20 20 25', text: '240 240 240',
|
|
17
|
+
border: '255 238 88', muted: '40 40 50', mutedForeground: '150 150 160',
|
|
18
|
+
input: '30 30 35', inputBorder: '80 80 90', ring: '255 238 88'
|
|
19
|
+
},
|
|
20
|
+
borderRadius: '0px',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="absolute top-4 right-4 flex gap-2 z-50">
|
|
25
|
+
{availableThemes.slice(0, 3).map(t => (
|
|
26
|
+
<Button key={t} size="sm" variant="ghost" onClick={() => setTheme(t)} className="opacity-50 hover:opacity-100">
|
|
27
|
+
{t}
|
|
28
|
+
</Button>
|
|
29
|
+
))}
|
|
30
|
+
<Button size="sm" variant="ghost" onClick={() => setCustomTheme(customTheme ? null : cyberTheme)} className="opacity-50 hover:opacity-100">
|
|
31
|
+
AI
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const AuthLayout: React.FC = () => {
|
|
38
|
+
return (
|
|
39
|
+
<div className="min-h-screen w-full flex items-center justify-center bg-background text-foreground relative transition-colors duration-300">
|
|
40
|
+
|
|
41
|
+
{/* Background Pattern */}
|
|
42
|
+
<div className="absolute inset-0 z-0 opacity-[0.03] pointer-events-none bg-[radial-gradient(#888_1px,transparent_1px)] [background-size:16px_16px]" />
|
|
43
|
+
|
|
44
|
+
<MiniThemeSwitcher />
|
|
45
|
+
|
|
46
|
+
{/* ✅ FIX: Removed the outer <Card>, Header, and Footer.
|
|
47
|
+
This is now a "Passthrough" layout. It centers the content
|
|
48
|
+
but lets the Page (LoginPage/SignupPage) own the UI completely.
|
|
49
|
+
*/}
|
|
50
|
+
<div className="z-10 relative w-full flex justify-center p-4">
|
|
51
|
+
<Outlet />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|