@ramme-io/create-app 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramme-io/create-app",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-ramme-app": "./index.js"
@@ -1,14 +1,14 @@
1
1
  import { Routes, Route, Navigate } from 'react-router-dom';
2
2
  import { generateRoutes } from './core/route-generator';
3
- import { useEffect } from 'react'; // <-- 1. Import useEffect
3
+ import { useEffect } from 'react';
4
4
 
5
5
  // --- 1. IMPORT ALL THREE TEMPLATES ---
6
6
  import DashboardLayout from './templates/dashboard/DashboardLayout';
7
- import { dashboardSitemap as dashboardSitemap } from './templates/dashboard/dashboard.sitemap';
7
+ import { dashboardSitemap } from './templates/dashboard/dashboard.sitemap';
8
8
  import DocsLayout from './templates/docs/DocsLayout';
9
- import { docsSitemap as docsSitemap } from './templates/docs/docs.sitemap';
9
+ import { docsSitemap } from './templates/docs/docs.sitemap';
10
10
  import SettingsLayout from './templates/settings/SettingsLayout';
11
- import { settingsSitemap as settingsSitemap } from './templates/settings/settings.sitemap';
11
+ import { settingsSitemap } from './templates/settings/settings.sitemap';
12
12
 
13
13
  // Other Imports
14
14
  import ProtectedRoute from './components/ProtectedRoute';
@@ -16,12 +16,11 @@ import LoginPage from './pages/LoginPage';
16
16
  import NotFound from './pages/styleguide/NotFound';
17
17
 
18
18
  // --- 2. IMPORT THE SEEDER ---
19
- import { initializeDataLake } from './core/data-seeder';
19
+ import { initializeDataLake } from './core/data-seeder';
20
20
 
21
21
  function App() {
22
22
 
23
23
  // ✅ 3. TRIGGER DATA SEEDING ON MOUNT
24
- // This checks localStorage and injects our mock database if missing.
25
24
  useEffect(() => {
26
25
  initializeDataLake();
27
26
  }, []);
@@ -30,8 +29,8 @@ function App() {
30
29
  <Routes>
31
30
  <Route path="/login" element={<LoginPage />} />
32
31
  <Route element={<ProtectedRoute />}>
33
- {/* Default redirect to the dashboard's root */}
34
- <Route path="/" element={<Navigate to="/dashboard" replace />} />
32
+ {/* FIX: Redirect root to the new Welcome page */}
33
+ <Route path="/" element={<Navigate to="/dashboard/welcome" replace />} />
35
34
 
36
35
  {/* Dashboard Template Routes */}
37
36
  <Route path="/dashboard/*" element={<DashboardLayout />}>
@@ -1,183 +1,216 @@
1
- import React, { useState, useMemo } from 'react';
2
- import { DataTable, Button, Icon, Card, useToast, type ColDef } from '@ramme-io/ui';
1
+ import React, { useState, useMemo, useCallback } from 'react';
2
+ import {
3
+ DataTable,
4
+ Button,
5
+ Icon,
6
+ Card,
7
+ useToast,
8
+ SearchInput,
9
+ type ColDef,
10
+ type GridApi
11
+ } from '@ramme-io/ui';
3
12
  import { getResourceMeta, getMockData } from '../data/mockData';
4
13
  import { AutoForm } from '../components/AutoForm';
5
- // ✅ Import types that now definitely exist
6
- import { useDataQuery, type FilterOption, type SortOption } from '../hooks/useDataQuery';
7
14
  import { useCrudLocalStorage } from '../hooks/useCrudLocalStorage';
8
15
 
9
16
  interface SmartTableProps {
10
17
  dataId: string;
11
18
  title?: string;
12
19
  initialFilter?: Record<string, any>;
13
- initialSort?: SortOption;
14
20
  }
15
21
 
16
22
  export const SmartTable: React.FC<SmartTableProps> = ({
17
23
  dataId,
18
24
  title,
19
- initialFilter,
20
- initialSort
25
+ initialFilter
21
26
  }) => {
22
27
  const { addToast } = useToast();
23
28
 
24
- // 1. STATE
25
- const [page, setPage] = useState(1);
26
- const [pageSize] = useState(10); // Kept as state in case we add a selector later
27
-
28
- // 2. METADATA
29
+ // 1. DATA KERNEL
29
30
  const meta = getResourceMeta(dataId);
30
-
31
- // 3. STORAGE LAYER
32
- // Seed data if local storage is empty
33
31
  const seedData = useMemo(() => getMockData(dataId) || [], [dataId]);
34
32
 
33
+ // We use the CRUD hook to persist changes to localStorage
35
34
  const {
36
- data: rawData,
35
+ data: rowData,
37
36
  createItem,
38
- updateItem
37
+ updateItem,
38
+ deleteItem
39
39
  } = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
40
40
 
41
- // 4. LOGIC LAYER
42
- const activeFilters = useMemo<FilterOption[]>(() => {
43
- if (!initialFilter) return [];
44
- return Object.entries(initialFilter).map(([key, value]) => ({
45
- field: key,
46
- operator: 'equals',
47
- value
48
- }));
49
- }, [initialFilter]);
50
-
51
- const { data: processedRows, total, pageCount } = useDataQuery(rawData, {
52
- filters: activeFilters,
53
- sort: initialSort, // Using the prop directly for now (Fixes 'setSort' unused)
54
- page,
55
- pageSize
56
- });
57
-
58
- // 5. UI STATE
41
+ // 2. UI STATE
59
42
  const [isEditOpen, setIsEditOpen] = useState(false);
60
43
  const [currentRecord, setCurrentRecord] = useState<any>(null);
44
+ const [gridApi, setGridApi] = useState<GridApi | null>(null);
45
+ const [selectedRows, setSelectedRows] = useState<any[]>([]);
46
+ const [quickFilterText, setQuickFilterText] = useState('');
61
47
 
62
- // 6. COLUMNS
63
- const columns = useMemo(() => {
64
- if (meta?.fields) {
65
- const cols: ColDef[] = meta.fields.map((f: any) => {
66
- const colDef: ColDef = {
67
- field: f.key,
68
- headerName: f.label,
69
- filter: true,
70
- sortable: true,
71
- flex: 1,
72
- };
48
+ // 3. COLUMN DEFINITIONS
49
+ const columns = useMemo<ColDef[]>(() => {
50
+ if (!meta?.fields) return [];
73
51
 
74
- if (f.key.endsWith('Id')) {
75
- const collectionName = f.key.replace('Id', 's');
76
- const relatedData = getMockData(collectionName);
77
- if (relatedData) {
78
- colDef.valueFormatter = (params) => {
79
- const match = relatedData.find((item: any) => item.id === params.value);
80
- return match ? (match.name || match.title || params.value) : params.value;
81
- };
82
- }
83
- }
84
-
85
- if (f.type === 'currency') colDef.valueFormatter = (p: any) => p.value ? `$${p.value}` : '';
86
- if (f.type === 'date') colDef.valueFormatter = (p: any) => p.value ? new Date(p.value).toLocaleDateString() : '';
87
-
88
- if (f.type === 'status') {
89
- colDef.cellRenderer = (p: any) => (
90
- <span className={`px-2 py-1 rounded-full text-xs font-medium border
91
- ${['active', 'paid'].includes(p.value?.toLowerCase()) ? 'bg-green-100 text-green-800 border-green-200' :
92
- ['pending'].includes(p.value?.toLowerCase()) ? 'bg-yellow-100 text-yellow-800 border-yellow-200' : 'bg-slate-100 text-slate-800 border-slate-200'}`}>
52
+ const generatedCols: ColDef[] = meta.fields.map((f: any) => {
53
+ const col: ColDef = {
54
+ field: f.key,
55
+ headerName: f.label,
56
+ filter: true,
57
+ sortable: true,
58
+ resizable: true,
59
+ flex: 1,
60
+ };
61
+
62
+ // Smart Formatting based on type
63
+ if (f.type === 'currency') {
64
+ col.valueFormatter = (p: any) => p.value ? `$${Number(p.value).toLocaleString()}` : '';
65
+ }
66
+ if (f.type === 'date') {
67
+ col.valueFormatter = (p: any) => p.value ? new Date(p.value).toLocaleDateString() : '';
68
+ }
69
+ if (f.type === 'status') {
70
+ col.cellRenderer = (p: any) => {
71
+ const statusColors: any = {
72
+ active: 'bg-green-100 text-green-800',
73
+ paid: 'bg-green-100 text-green-800',
74
+ pending: 'bg-yellow-100 text-yellow-800',
75
+ inactive: 'bg-slate-100 text-slate-600',
76
+ overdue: 'bg-red-100 text-red-800'
77
+ };
78
+ const colorClass = statusColors[String(p.value).toLowerCase()] || 'bg-slate-100 text-slate-800';
79
+ return (
80
+ <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
93
81
  {p.value}
94
82
  </span>
95
83
  );
96
- }
97
- return colDef;
98
- });
99
-
100
- cols.push({
101
- headerName: "Actions",
102
- field: "id",
103
- width: 100,
104
- pinned: 'right',
105
- cellRenderer: (params: any) => (
106
- <Button variant="ghost" size="sm" onClick={() => { setCurrentRecord(params.data); setIsEditOpen(true); }}>
107
- <Icon name="edit-2" className="w-4 h-4 text-slate-500" />
84
+ };
85
+ }
86
+ return col;
87
+ });
88
+
89
+ // Add Checkbox Selection to the first column
90
+ if (generatedCols.length > 0) {
91
+ generatedCols[0].headerCheckboxSelection = true;
92
+ generatedCols[0].checkboxSelection = true;
93
+ generatedCols[0].minWidth = 180;
94
+ }
95
+
96
+ // Add Actions Column
97
+ generatedCols.push({
98
+ headerName: "Actions",
99
+ field: "id",
100
+ width: 100,
101
+ pinned: 'right',
102
+ cellRenderer: (params: any) => (
103
+ <div className="flex items-center gap-1">
104
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={(e) => { e.stopPropagation(); setCurrentRecord(params.data); setIsEditOpen(true); }}>
105
+ <Icon name="edit-2" size={14} className="text-slate-500" />
108
106
  </Button>
109
- )
110
- });
111
- return cols;
107
+ </div>
108
+ )
109
+ });
110
+
111
+ return generatedCols;
112
+ }, [meta]);
113
+
114
+ // 4. HANDLERS
115
+ const onGridReady = useCallback((params: any) => {
116
+ setGridApi(params.api);
117
+ }, []);
118
+
119
+ const onSelectionChanged = useCallback(() => {
120
+ if (gridApi) {
121
+ setSelectedRows(gridApi.getSelectedRows());
112
122
  }
113
-
114
- if (processedRows.length > 0) {
115
- return Object.keys(processedRows[0]).map(k => ({ field: k, headerName: k.toUpperCase(), flex: 1 }));
123
+ }, [gridApi]);
124
+
125
+ const handleBulkDelete = () => {
126
+ if (confirm(`Delete ${selectedRows.length} items?`)) {
127
+ selectedRows.forEach(row => deleteItem(row.id));
128
+ setSelectedRows([]);
129
+ addToast(`${selectedRows.length} items deleted`, 'success');
116
130
  }
117
- return [];
118
- }, [meta, processedRows]);
131
+ };
119
132
 
120
133
  const handleSave = (record: any) => {
121
134
  if (record.id && currentRecord?.id) {
122
- updateItem(record);
123
- addToast('Record updated successfully', 'success');
135
+ updateItem(record);
136
+ addToast('Item updated', 'success');
124
137
  } else {
125
- const { id, ...newItem } = record;
126
- createItem(newItem);
127
- addToast('New record created', 'success');
138
+ const { id, ...newItem } = record;
139
+ createItem(newItem);
140
+ addToast('Item created', 'success');
128
141
  }
129
142
  setIsEditOpen(false);
130
143
  };
131
144
 
145
+ // --- RENDER ---
132
146
  return (
133
- <Card className="p-0 overflow-hidden border border-border flex flex-col h-full min-h-[500px]">
134
- <div className="p-4 border-b border-border flex justify-between items-center bg-muted/20">
147
+ <Card className="flex flex-col h-[600px] border border-border shadow-sm overflow-hidden bg-card">
148
+
149
+ {/* HEADER TOOLBAR */}
150
+ <div className="p-4 border-b border-border flex justify-between items-center gap-4 bg-muted/5">
151
+
152
+ {/* Left: Title or Bulk Actions */}
153
+ {selectedRows.length > 0 ? (
154
+ <div className="flex items-center gap-3 animate-in fade-in slide-in-from-left-2 duration-200">
155
+ <span className="bg-primary text-primary-foreground text-xs font-bold px-2 py-1 rounded-md">
156
+ {selectedRows.length} Selected
157
+ </span>
158
+ <Button size="sm" variant="danger" onClick={handleBulkDelete} iconLeft="trash-2">
159
+ Delete Selected
160
+ </Button>
161
+ </div>
162
+ ) : (
163
+ <div className="flex items-center gap-2">
164
+ <div className="p-2 bg-primary/10 rounded-md text-primary">
165
+ <Icon name="table" size={18} />
166
+ </div>
167
+ <div>
168
+ <h3 className="text-base font-bold text-foreground leading-tight">
169
+ {title || meta?.name || dataId}
170
+ </h3>
171
+ <p className="text-xs text-muted-foreground">
172
+ {rowData.length} records found
173
+ </p>
174
+ </div>
175
+ </div>
176
+ )}
177
+
178
+ {/* Right: Actions & Filter */}
135
179
  <div className="flex items-center gap-2">
136
- <h3 className="text-lg font-semibold text-foreground">
137
- {title || meta?.name || dataId}
138
- </h3>
139
- <span className="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full">
140
- {total} records
141
- </span>
180
+ <div className="w-64">
181
+ <SearchInput
182
+ placeholder="Quick search..."
183
+ value={quickFilterText}
184
+ onChange={(e) => {
185
+ setQuickFilterText(e.target.value);
186
+ gridApi?.setQuickFilter(e.target.value);
187
+ }}
188
+ />
189
+ </div>
190
+ <div className="h-6 w-px bg-border mx-1" />
191
+ <Button size="sm" variant="primary" iconLeft="plus" onClick={() => { setCurrentRecord({}); setIsEditOpen(true); }}>
192
+ Add New
193
+ </Button>
142
194
  </div>
143
- <Button size="sm" variant="outline" onClick={() => { setCurrentRecord({}); setIsEditOpen(true); }}>
144
- <Icon name="plus" className="mr-2 w-4 h-4" /> Add Item
145
- </Button>
146
195
  </div>
147
196
 
197
+ {/* AG GRID */}
148
198
  <div className="flex-1 w-full bg-card relative">
149
199
  <DataTable
150
- rowData={processedRows}
200
+ rowData={rowData}
151
201
  columnDefs={columns}
152
- pagination={false} // We handle pagination logic manually below
202
+ onGridReady={onGridReady}
203
+ onSelectionChanged={onSelectionChanged}
204
+ rowSelection="multiple"
205
+ pagination={true}
206
+ paginationPageSize={10}
207
+ headerHeight={48}
208
+ rowHeight={48}
209
+ enableCellTextSelection={true}
153
210
  />
154
-
155
- {/* Pagination Controls */}
156
- <div className="p-2 border-t border-border flex justify-between items-center text-sm">
157
- <span className="text-muted-foreground">
158
- Page {page} of {pageCount || 1}
159
- </span>
160
- <div className="flex gap-2">
161
- <Button
162
- variant="ghost"
163
- size="sm"
164
- disabled={page === 1}
165
- onClick={() => setPage(p => Math.max(1, p - 1))}
166
- >
167
- Prev
168
- </Button>
169
- <Button
170
- variant="ghost"
171
- size="sm"
172
- disabled={page >= pageCount}
173
- onClick={() => setPage(p => p + 1)}
174
- >
175
- Next
176
- </Button>
177
- </div>
178
- </div>
179
211
  </div>
180
212
 
213
+ {/* EDIT DRAWER */}
181
214
  <AutoForm
182
215
  isOpen={isEditOpen}
183
216
  onClose={() => setIsEditOpen(false)}
@@ -1,40 +1,67 @@
1
- import { useSignal } from '../hooks/useSignal';
1
+ import { create } from 'zustand';
2
+ import { useEffect } from 'react';
2
3
 
3
- export const useGeneratedSignals = () => {
4
+ // --- 1. SIGNAL STORE ---
5
+ export interface SignalValue {
6
+ value: any;
7
+ timestamp: number;
8
+ }
9
+
10
+ interface SignalStore {
11
+ signals: Record<string, SignalValue>;
12
+ updateSignal: (id: string, value: any) => void;
13
+ updateSignals: (updates: Record<string, any>) => void;
14
+ }
15
+
16
+ // Initial State matches app.manifest.ts
17
+ const initialState = {
18
+ living_room_ac: { value: 72, timestamp: Date.now() },
19
+ living_room_hum: { value: 45, timestamp: Date.now() },
20
+ server_01: { value: 42, timestamp: Date.now() },
21
+ front_door_lock: { value: 'LOCKED', timestamp: Date.now() }
22
+ };
23
+
24
+ export const useSignalStore = create<SignalStore>((set) => ({
25
+ signals: initialState,
4
26
 
5
- // 🟢 REAL: Connected to public MQTT test broker
6
- const living_room_ac = useSignal('living_room_ac', {
7
- initialValue: 72,
8
- min: 60,
9
- max: 90,
10
- unit: '°F',
11
- topic: 'ramme/test/temp' // <--- The magic link
12
- });
13
-
14
- // 🟠 MOCK: Simulation Mode
15
- const living_room_hum = useSignal('living_room_hum', {
16
- initialValue: 45,
17
- min: 40,
18
- max: 60,
19
- unit: '%'
20
- });
21
-
22
- const server_01 = useSignal('server_01', {
23
- initialValue: 42,
24
- min: 10,
25
- max: 95,
26
- unit: '%'
27
- });
28
-
29
- const front_door_lock = useSignal('front_door_lock', {
30
- initialValue: 'LOCKED',
31
- unit: ''
32
- });
33
-
34
- return {
35
- living_room_ac,
36
- living_room_hum,
37
- server_01,
38
- front_door_lock,
39
- };
27
+ updateSignal: (id, value) => set((state) => ({
28
+ signals: {
29
+ ...state.signals,
30
+ [id]: { value, timestamp: Date.now() }
31
+ }
32
+ })),
33
+
34
+ updateSignals: (updates) => set((state) => {
35
+ const newSignals = { ...state.signals };
36
+ Object.entries(updates).forEach(([id, val]) => {
37
+ newSignals[id] = { value: val, timestamp: Date.now() };
38
+ });
39
+ return { signals: newSignals };
40
+ })
41
+ }));
42
+
43
+ export const useGeneratedSignals = () => {
44
+ return useSignalStore((state) => state.signals);
45
+ };
46
+
47
+ // --- 2. SIMULATION ENGINE ---
48
+ export const useSimulation = () => {
49
+ const { updateSignals } = useSignalStore();
50
+
51
+ useEffect(() => {
52
+ console.log("[System] Simulation Started");
53
+ const interval = setInterval(() => {
54
+ // Simulate random fluctuations for sensors
55
+ const updates: Record<string, any> = {};
56
+
57
+ // Randomize values slightly
58
+ updates['living_room_ac'] = Number((72 + (Math.random() * 4 - 2)).toFixed(1));
59
+ updates['living_room_hum'] = Number((45 + (Math.random() * 6 - 3)).toFixed(1));
60
+ updates['server_01'] = Math.floor(Math.random() * 100);
61
+
62
+ updateSignals(updates);
63
+ }, 2000); // Update every 2 seconds
64
+
65
+ return () => clearInterval(interval);
66
+ }, [updateSignals]);
40
67
  };
@@ -1,6 +1,123 @@
1
- // Placeholder for the Generated Engine
1
+ import { useEffect } from 'react';
2
+ import { useToast } from '@ramme-io/ui';
3
+ // @ts-ignore - These generated hooks exist in the build
4
+ import { useGeneratedSignals, useSimulation } from '../generated/hooks';
5
+ import { appManifest } from '../config/app.manifest';
6
+ // ✅ Import types to fix implicit 'any' errors
7
+ import type { ActionDefinition, WorkflowDefinition } from '../types/schema';
8
+
9
+ /**
10
+ * THE RUNTIME LOGIC ENGINE
11
+ * This hook breathes life into the application.
12
+ * It watches for signal changes and executes the workflows defined in the manifest.
13
+ */
2
14
  export const useWorkflowEngine = () => {
3
- // This will be overwritten by the Builder at runtime
4
- console.log(" [Mock] Workflow Engine Mounted");
5
- return {};
15
+ const signals = useGeneratedSignals();
16
+ const { addToast } = useToast();
17
+
18
+ // 1. Activate the Data Simulation (Randomizer)
19
+ useSimulation();
20
+
21
+ // 2. Define the Action Executor
22
+ // ✅ Explicitly typed 'action'
23
+ const executeAction = async (action: ActionDefinition, context: any) => {
24
+ console.log(`[Engine] Executing: ${action.type}`, action);
25
+
26
+ switch (action.type) {
27
+ case 'send_notification':
28
+ addToast(action.config.message || 'Notification Sent', 'info');
29
+ break;
30
+
31
+ case 'update_resource':
32
+ // In a real app, this would call the API
33
+ addToast(`Updating Resource: ${JSON.stringify(action.config)}`, 'success');
34
+ break;
35
+
36
+ case 'agent_task':
37
+ // Simulate AI Agent processing
38
+ console.log(`[AI Agent] Thinking about: "${action.config.prompt}"...`);
39
+ // ✅ FIX: 'loading' is not a valid ToastType. Using 'info' instead.
40
+ addToast('AI Agent Analyzing...', 'info');
41
+
42
+ setTimeout(() => {
43
+ // Mock response based on prompt context
44
+ const response = action.config.prompt?.includes('health')
45
+ ? "System Operating Normally."
46
+ : "Optimization Recommended.";
47
+
48
+ addToast(`🤖 Agent: "${response}"`, 'success', 5000);
49
+ }, 1500);
50
+ break;
51
+
52
+ case 'navigate':
53
+ window.location.href = action.config.path;
54
+ break;
55
+
56
+ default:
57
+ console.warn('Unknown action type:', action.type);
58
+ }
59
+ };
60
+
61
+ // 3. Watch Signals & Trigger Workflows
62
+ useEffect(() => {
63
+ if (!appManifest.domain?.workflows) return;
64
+
65
+ // ✅ Explicitly typed 'flow'
66
+ appManifest.domain.workflows.forEach((flow: WorkflowDefinition) => {
67
+ if (!flow.active) return;
68
+
69
+ // Check Trigger: Signal Change
70
+ if (flow.trigger.type === 'signal_change') {
71
+ const signalId = flow.trigger.config.signalId;
72
+ const condition = flow.trigger.config.condition; // e.g., "> 80"
73
+
74
+ // @ts-ignore
75
+ const signal = signals[signalId];
76
+
77
+ if (signal) {
78
+ const val = typeof signal === 'object' ? signal.value : signal;
79
+
80
+ try {
81
+ // Check if condition is met (e.g. "50 > 80")
82
+ const isMet = checkCondition(val, condition);
83
+
84
+ if (isMet) {
85
+ // Throttling logic would go here to prevent spam
86
+ console.log(`[Engine] Trigger Fired: ${flow.name}`);
87
+ flow.actions.forEach(action => executeAction(action, { signal: val }));
88
+ }
89
+ } catch (e) {
90
+ // Ignore parse errors
91
+ }
92
+ }
93
+ }
94
+ });
95
+ }, [signals, addToast]);
96
+
97
+ return {
98
+ // Exposed for manual triggering (e.g. Buttons)
99
+ triggerWorkflow: (workflowId: string) => {
100
+ const flow = appManifest.domain?.workflows?.find(w => w.id === workflowId);
101
+ if (flow) {
102
+ flow.actions.forEach(action => executeAction(action, { manual: true }));
103
+ }
104
+ }
105
+ };
106
+ };
107
+
108
+ // --- HELPER: Safe Condition Checker ---
109
+ const checkCondition = (value: number, condition: string): boolean => {
110
+ if (!condition) return false;
111
+ const parts = condition.trim().split(' ');
112
+ const operator = parts[0];
113
+ const target = parseFloat(parts[1]);
114
+
115
+ switch (operator) {
116
+ case '>': return value > target;
117
+ case '<': return value < target;
118
+ case '>=': return value >= target;
119
+ case '<=': return value <= target;
120
+ case '==': return value === target;
121
+ default: return false;
122
+ }
6
123
  };
@@ -0,0 +1,162 @@
1
+ import React from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import {
4
+ PageHeader,
5
+ Card,
6
+ Button,
7
+ Icon,
8
+ Badge,
9
+ SectionHeader
10
+ } from '@ramme-io/ui';
11
+
12
+ const Welcome: React.FC = () => {
13
+ const navigate = useNavigate();
14
+
15
+ return (
16
+ <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
17
+
18
+ {/* HERO */}
19
+ <div className="relative overflow-hidden rounded-xl bg-gradient-to-r from-primary to-violet-600 text-primary-foreground p-8 md:p-12">
20
+ <div className="relative z-10 max-w-2xl">
21
+ <Badge variant="secondary" className="mb-4 bg-white/20 text-white border-none backdrop-blur-sm">
22
+ v1.2.0 Starter Kit
23
+ </Badge>
24
+ <h1 className="text-4xl md:text-5xl font-extrabold mb-4 tracking-tight">
25
+ Your Ramme App is Ready.
26
+ </h1>
27
+ <p className="text-lg md:text-xl text-primary-foreground/90 mb-8 leading-relaxed">
28
+ You have successfully scaffolded a production-ready prototype environment.
29
+ This kit comes pre-wired with Authentication, Mock Data, and the A.D.A.P.T. architecture.
30
+ </p>
31
+ <div className="flex flex-wrap gap-4">
32
+ <Button
33
+ size="lg"
34
+ variant="secondary"
35
+ className="font-semibold"
36
+ iconLeft="layout-dashboard"
37
+ onClick={() => navigate('/dashboard/app')}
38
+ >
39
+ Open Live Dashboard
40
+ </Button>
41
+ <Button
42
+ size="lg"
43
+ variant="outline"
44
+ className="bg-transparent border-white/30 text-white hover:bg-white/10 hover:text-white"
45
+ iconLeft="book-open"
46
+ onClick={() => navigate('/docs')}
47
+ >
48
+ Read Documentation
49
+ </Button>
50
+ </div>
51
+ </div>
52
+
53
+ {/* Decorative Background Icon */}
54
+ <div className="absolute -right-10 -bottom-10 opacity-10 rotate-12">
55
+ <Icon name="box" size={300} />
56
+ </div>
57
+ </div>
58
+
59
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
60
+
61
+ {/* SECTION 1: ARCHITECTURE */}
62
+ <div className="space-y-4">
63
+ <SectionHeader title="Project Architecture" />
64
+ <div className="grid gap-4">
65
+ <Card className="p-4 flex gap-4 hover:border-primary/50 transition-colors cursor-default">
66
+ <div className="p-3 rounded-lg bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 h-fit">
67
+ <Icon name="database" size={24} />
68
+ </div>
69
+ <div>
70
+ <h3 className="font-semibold text-foreground">Data Lake</h3>
71
+ <p className="text-sm text-muted-foreground mt-1">
72
+ Mock data is seeded into <code>localStorage</code> on boot.
73
+ Edit <code>src/data/mockData.ts</code> to change the schema.
74
+ </p>
75
+ </div>
76
+ </Card>
77
+
78
+ <Card className="p-4 flex gap-4 hover:border-primary/50 transition-colors cursor-default">
79
+ <div className="p-3 rounded-lg bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400 h-fit">
80
+ <Icon name="git-branch" size={24} />
81
+ </div>
82
+ <div>
83
+ <h3 className="font-semibold text-foreground">Logic Engine</h3>
84
+ <p className="text-sm text-muted-foreground mt-1">
85
+ Workflows and signals are processed in real-time by
86
+ <code>useWorkflowEngine.ts</code>. Supports MQTT and simulated sensors.
87
+ </p>
88
+ </div>
89
+ </Card>
90
+
91
+ <Card className="p-4 flex gap-4 hover:border-primary/50 transition-colors cursor-default">
92
+ <div className="p-3 rounded-lg bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400 h-fit">
93
+ <Icon name="layout" size={24} />
94
+ </div>
95
+ <div>
96
+ <h3 className="font-semibold text-foreground">Dynamic Routing</h3>
97
+ <p className="text-sm text-muted-foreground mt-1">
98
+ Pages are generated from <code>app.manifest.ts</code>.
99
+ Visual blocks connect automatically to data sources.
100
+ </p>
101
+ </div>
102
+ </Card>
103
+ </div>
104
+ </div>
105
+
106
+ {/* SECTION 2: RESOURCES */}
107
+ <div className="space-y-4">
108
+ <SectionHeader title="Developer Resources" />
109
+
110
+ <Card className="p-6 space-y-6">
111
+ <div>
112
+ <h4 className="font-semibold flex items-center gap-2 mb-2">
113
+ <Icon name="palette" size={16} className="text-muted-foreground" />
114
+ Component Library
115
+ </h4>
116
+ <p className="text-sm text-muted-foreground mb-3">
117
+ Browse the full suite of accessible UI components available in this project.
118
+ </p>
119
+ <Button variant="outline" size="sm" onClick={() => navigate('/styleguide')}>
120
+ View Style Guide
121
+ </Button>
122
+ </div>
123
+
124
+ <div className="border-t border-border pt-6">
125
+ <h4 className="font-semibold flex items-center gap-2 mb-2">
126
+ <Icon name="settings" size={16} className="text-muted-foreground" />
127
+ Configuration
128
+ </h4>
129
+ <p className="text-sm text-muted-foreground mb-3">
130
+ Manage global settings, user profile templates, and billing layouts.
131
+ </p>
132
+ <Button variant="outline" size="sm" onClick={() => navigate('/settings')}>
133
+ Open Settings
134
+ </Button>
135
+ </div>
136
+
137
+ <div className="border-t border-border pt-6">
138
+ <h4 className="font-semibold flex items-center gap-2 mb-2">
139
+ <Icon name="github" size={16} className="text-muted-foreground" />
140
+ Community
141
+ </h4>
142
+ <p className="text-sm text-muted-foreground mb-3">
143
+ Need help? Check the docs or open an issue on GitHub.
144
+ </p>
145
+ <a
146
+ href="https://github.com/ramme-io/create-app"
147
+ target="_blank"
148
+ rel="noreferrer"
149
+ className="inline-flex"
150
+ >
151
+ <Button variant="ghost" size="sm">GitHub Repo &rarr;</Button>
152
+ </a>
153
+ </div>
154
+ </Card>
155
+ </div>
156
+
157
+ </div>
158
+ </div>
159
+ );
160
+ };
161
+
162
+ export default Welcome;
@@ -2,8 +2,18 @@ import { type SitemapEntry } from '../../core/sitemap-entry';
2
2
  import { appManifest } from '../../config/app.manifest';
3
3
  import Dashboard from '../../pages/Dashboard';
4
4
  import AiChat from '../../pages/AiChat';
5
+ import Welcome from '../../pages/Welcome'; // ✅ Import the new page
5
6
 
6
- export const dashboardSitemap: SitemapEntry[] = [];
7
+ export const dashboardSitemap: SitemapEntry[] = [
8
+ // ✅ 1. The New Landing Page
9
+ {
10
+ id: 'welcome',
11
+ path: 'welcome',
12
+ title: 'Start Here',
13
+ icon: 'rocket',
14
+ component: Welcome,
15
+ },
16
+ ];
7
17
 
8
18
  // A. Dynamic Pages from Manifest
9
19
  if (appManifest.pages) {
@@ -13,10 +23,11 @@ if (appManifest.pages) {
13
23
  dashboardSitemap.push({
14
24
  id: page.id,
15
25
  title: page.title,
16
- // Map root to Dashboard, others to their slug
17
- path: isDashboard ? '' : page.slug,
26
+ // ✅ FIX: Map the main dashboard to 'app' instead of root ''
27
+ // This prevents conflict with the layout root
28
+ path: isDashboard ? 'app' : page.slug,
18
29
  icon: isDashboard ? 'layout-dashboard' : 'file-text',
19
- component: Dashboard, // Map everything to the Universal Renderer
30
+ component: Dashboard,
20
31
  });
21
32
  });
22
33
  }
@@ -70,6 +70,39 @@ export const EntitySchema = z.object({
70
70
  });
71
71
  export type EntityDefinition = z.infer<typeof EntitySchema>;
72
72
 
73
+ // ------------------------------------------------------------------
74
+ // ✅ NEW: LOGIC & WORKFLOW DEFINITIONS
75
+ // ------------------------------------------------------------------
76
+
77
+ export const TriggerSchema = z.object({
78
+ id: z.string(),
79
+ type: z.enum(['signal_change', 'manual_action', 'schedule', 'webhook']),
80
+ config: z.record(z.string(), z.any()),
81
+ });
82
+
83
+ export const ActionSchema = z.object({
84
+ id: z.string(),
85
+ type: z.enum([
86
+ 'update_resource',
87
+ 'send_notification',
88
+ 'mqtt_publish',
89
+ 'api_call',
90
+ 'navigate',
91
+ 'agent_task'
92
+ ]),
93
+ config: z.record(z.string(), z.any()),
94
+ });
95
+
96
+ export const WorkflowSchema = z.object({
97
+ id: z.string(),
98
+ name: z.string(),
99
+ active: z.boolean().default(true),
100
+ trigger: TriggerSchema,
101
+ actions: z.array(ActionSchema),
102
+ });
103
+ export type WorkflowDefinition = z.infer<typeof WorkflowSchema>;
104
+ export type ActionDefinition = z.infer<typeof ActionSchema>;
105
+
73
106
 
74
107
  // ------------------------------------------------------------------
75
108
  // 3. UI LAYOUT DEFINITIONS (Presentation Layer)
@@ -78,7 +111,6 @@ export type EntityDefinition = z.infer<typeof EntitySchema>;
78
111
  export const BlockSchema = z.object({
79
112
  id: z.string(),
80
113
  type: z.string(),
81
- // ✅ FIX: Explicitly define Key and Value types
82
114
  props: z.record(z.string(), z.any()),
83
115
  layout: z.object({
84
116
  colSpan: z.number().optional(),
@@ -115,7 +147,7 @@ export const AppSpecificationSchema = z.object({
115
147
  version: z.string(),
116
148
  description: z.string().optional(),
117
149
  author: z.string().optional(),
118
- createdAt: z.string().optional(), // <--- ADD THIS LINE
150
+ createdAt: z.string().optional(),
119
151
  }),
120
152
 
121
153
  config: z.object({
@@ -130,6 +162,8 @@ export const AppSpecificationSchema = z.object({
130
162
  domain: z.object({
131
163
  signals: z.array(SignalSchema),
132
164
  entities: z.array(EntitySchema),
165
+ // ✅ ADDED WORKFLOWS HERE
166
+ workflows: z.array(WorkflowSchema).optional(),
133
167
  }),
134
168
 
135
169
  pages: z.array(PageSchema).optional(),