@ramme-io/create-app 1.2.2 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -14
- package/template/package.json +5 -3
- package/template/pkg.json +11 -7
- package/template/src/App.tsx +11 -1
- package/template/src/components/AppHeader.tsx +10 -10
- package/template/src/components/dashboard/ChartLine.tsx +28 -0
- package/template/src/components/dashboard/StatCard.tsx +61 -0
- package/template/src/config/component-registry.tsx +57 -44
- package/template/src/engine/renderers/DynamicBlock.tsx +1 -1
- package/template/src/engine/renderers/DynamicPage.tsx +134 -98
- package/template/src/engine/runtime/ManifestContext.tsx +79 -0
- package/template/src/engine/runtime/MqttContext.tsx +16 -21
- package/template/src/engine/runtime/data-seeder.ts +9 -7
- package/template/src/engine/runtime/useAction.ts +7 -21
- 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 +10 -21
- package/template/src/engine/runtime/useWorkflowEngine.ts +23 -78
- package/template/src/features/datagrid/SmartTable.tsx +38 -20
- package/template/src/features/developer/GhostOverlay.tsx +67 -21
- package/template/src/features/visualizations/SmartChart.tsx +178 -0
- package/template/src/main.tsx +25 -13
- package/template/src/templates/dashboard/DashboardLayout.tsx +19 -18
- package/template/src/templates/dashboard/dashboard.sitemap.ts +1 -8
- package/template/tailwind.config.cjs +10 -9
- package/template/vite.config.ts +0 -14
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BarChart, Bar, LineChart, Line, AreaChart, Area, PieChart, Pie, Cell,
|
|
4
|
+
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend
|
|
5
|
+
} from 'recharts';
|
|
6
|
+
import { Card } from '@ramme-io/ui';
|
|
7
|
+
import { BarChart3, Bug } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
interface SmartChartProps {
|
|
10
|
+
rowData?: any[];
|
|
11
|
+
chartType?: 'bar' | 'line' | 'area' | 'pie';
|
|
12
|
+
xAxis?: string;
|
|
13
|
+
yAxis?: string;
|
|
14
|
+
aggregation?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const COLORS = ['#0ea5e9', '#22c55e', '#eab308', '#ef4444', '#8b5cf6'];
|
|
20
|
+
|
|
21
|
+
export const SmartChart: React.FC<SmartChartProps> = ({
|
|
22
|
+
rowData = [], chartType = 'bar', xAxis, yAxis, aggregation = 'none', title, className
|
|
23
|
+
}) => {
|
|
24
|
+
|
|
25
|
+
// 1. DATA PROCESSING ENGINE
|
|
26
|
+
const processedData = useMemo(() => {
|
|
27
|
+
if (!rowData || rowData.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
// Fallback: If no xAxis provided, try to find a string key, else use 'id'
|
|
30
|
+
const activeX = xAxis || Object.keys(rowData[0]).find(k => typeof rowData[0][k] === 'string') || 'id';
|
|
31
|
+
|
|
32
|
+
// Normalize mode
|
|
33
|
+
const mode = aggregation?.toLowerCase() || 'none';
|
|
34
|
+
|
|
35
|
+
// SCENARIO A: COUNT
|
|
36
|
+
if (mode === 'count') {
|
|
37
|
+
const counts: Record<string, number> = {};
|
|
38
|
+
rowData.forEach(row => {
|
|
39
|
+
const rawVal = row[activeX];
|
|
40
|
+
// Handle Booleans (true/false) explicitly
|
|
41
|
+
const key = String(rawVal !== undefined && rawVal !== null ? rawVal : 'Unknown');
|
|
42
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
43
|
+
});
|
|
44
|
+
return Object.entries(counts).map(([name, value]) => ({ name, value }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// SCENARIO B: SUM
|
|
48
|
+
if (mode === 'sum' && yAxis) {
|
|
49
|
+
const sums: Record<string, number> = {};
|
|
50
|
+
rowData.forEach(row => {
|
|
51
|
+
const key = String(row[activeX] ?? 'Unknown');
|
|
52
|
+
const val = Number(row[yAxis]) || 0;
|
|
53
|
+
sums[key] = (sums[key] || 0) + val;
|
|
54
|
+
});
|
|
55
|
+
return Object.entries(sums).map(([name, value]) => ({ name, value }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// SCENARIO C: RAW
|
|
59
|
+
return rowData.map(row => ({
|
|
60
|
+
name: String(row[activeX] ?? ''),
|
|
61
|
+
value: yAxis ? Number(row[yAxis]) : 0
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
}, [rowData, xAxis, yAxis, aggregation]);
|
|
65
|
+
|
|
66
|
+
// 2. RENDERERS
|
|
67
|
+
const renderChart = () => {
|
|
68
|
+
// If no data to render, show placeholder
|
|
69
|
+
if (processedData.length === 0) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col items-center justify-center h-full text-muted-foreground opacity-50">
|
|
72
|
+
<BarChart3 size={32} />
|
|
73
|
+
<span className="text-xs mt-2">No Data Available</span>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const type = chartType?.toLowerCase() || 'bar';
|
|
79
|
+
switch (type) {
|
|
80
|
+
case 'pie':
|
|
81
|
+
return (
|
|
82
|
+
<PieChart>
|
|
83
|
+
<Pie data={processedData} cx="50%" cy="50%" innerRadius={60} outerRadius={80} paddingAngle={5} dataKey="value">
|
|
84
|
+
{processedData.map((_, index) => <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />)}
|
|
85
|
+
</Pie>
|
|
86
|
+
<Tooltip />
|
|
87
|
+
<Legend verticalAlign="bottom" height={36}/>
|
|
88
|
+
</PieChart>
|
|
89
|
+
);
|
|
90
|
+
case 'line':
|
|
91
|
+
return (
|
|
92
|
+
<LineChart data={processedData}>
|
|
93
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
|
|
94
|
+
<XAxis dataKey="name" fontSize={12} tickLine={false} axisLine={false} />
|
|
95
|
+
<YAxis fontSize={12} tickLine={false} axisLine={false} />
|
|
96
|
+
<Tooltip />
|
|
97
|
+
<Line type="monotone" dataKey="value" stroke="#0ea5e9" strokeWidth={3} dot={{r:4}} />
|
|
98
|
+
</LineChart>
|
|
99
|
+
);
|
|
100
|
+
case 'area':
|
|
101
|
+
return (
|
|
102
|
+
<AreaChart data={processedData}>
|
|
103
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
|
|
104
|
+
<XAxis dataKey="name" fontSize={12} tickLine={false} axisLine={false} />
|
|
105
|
+
<YAxis fontSize={12} tickLine={false} axisLine={false} />
|
|
106
|
+
<Tooltip />
|
|
107
|
+
<Area type="monotone" dataKey="value" stroke="#0ea5e9" fill="#0ea5e9" fillOpacity={0.2} />
|
|
108
|
+
</AreaChart>
|
|
109
|
+
);
|
|
110
|
+
case 'bar': default:
|
|
111
|
+
return (
|
|
112
|
+
<BarChart data={processedData}>
|
|
113
|
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
|
|
114
|
+
<XAxis dataKey="name" fontSize={12} tickLine={false} axisLine={false} />
|
|
115
|
+
<YAxis fontSize={12} tickLine={false} axisLine={false} />
|
|
116
|
+
<Tooltip cursor={{ fill: '#f1f5f9' }} />
|
|
117
|
+
<Bar dataKey="value" fill="#0ea5e9" radius={[4, 4, 0, 0]} />
|
|
118
|
+
</BarChart>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Card className={`p-4 flex flex-col h-full bg-white border border-border shadow-sm ${className}`}>
|
|
125
|
+
{/* Title Header */}
|
|
126
|
+
<div className="flex items-center justify-between mb-4">
|
|
127
|
+
<div className="flex flex-col">
|
|
128
|
+
{title && <h3 className="text-sm font-bold text-foreground uppercase tracking-wider">{title}</h3>}
|
|
129
|
+
{/* Subtitle with Aggregation info */}
|
|
130
|
+
{aggregation && aggregation !== 'none' && (
|
|
131
|
+
<span className="text-[10px] text-muted-foreground font-mono">
|
|
132
|
+
BY {aggregation.toUpperCase()} ({xAxis})
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Chart Canvas */}
|
|
139
|
+
<div className="flex-1 w-full min-h-[200px]">
|
|
140
|
+
<ResponsiveContainer width="100%" height="100%">{renderChart()}</ResponsiveContainer>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* ✅ DEBUG CONSOLE (Collapsible) */}
|
|
144
|
+
<div className="mt-4 border-t border-border pt-2">
|
|
145
|
+
<details className="text-[10px] text-muted-foreground group">
|
|
146
|
+
<summary className="cursor-pointer hover:text-primary flex items-center gap-2 select-none font-mono">
|
|
147
|
+
<Bug size={12} />
|
|
148
|
+
<span>Debug Configuration</span>
|
|
149
|
+
<span className="ml-auto opacity-50">{rowData?.length || 0} Rows</span>
|
|
150
|
+
</summary>
|
|
151
|
+
<div className="mt-2 p-3 bg-slate-950 text-green-400 rounded-md font-mono overflow-x-auto text-[10px] leading-relaxed shadow-inner">
|
|
152
|
+
<div className="mb-2 pb-2 border-b border-white/10">
|
|
153
|
+
<span className="text-white font-bold block mb-1">INCOMING PROPS:</span>
|
|
154
|
+
<div>chartType: <span className="text-yellow-300">"{chartType}"</span></div>
|
|
155
|
+
<div>aggregation: <span className="text-yellow-300">"{aggregation}"</span></div>
|
|
156
|
+
<div>xAxis (Key): <span className="text-yellow-300">"{xAxis}"</span></div>
|
|
157
|
+
<div>yAxis (Val): <span className="text-yellow-300">"{yAxis}"</span></div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="mb-2 pb-2 border-b border-white/10">
|
|
161
|
+
<span className="text-white font-bold block mb-1">DATA SNAPSHOT (Row 0):</span>
|
|
162
|
+
{rowData && rowData.length > 0 ? (
|
|
163
|
+
<pre>{JSON.stringify(rowData[0], null, 2)}</pre>
|
|
164
|
+
) : (
|
|
165
|
+
<span className="text-red-400">No Data Rows Received</span>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div>
|
|
170
|
+
<span className="text-white font-bold block mb-1">PROCESSED OUTPUT (Top 3):</span>
|
|
171
|
+
<pre>{JSON.stringify(processedData.slice(0, 3), null, 2)}</pre>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</details>
|
|
175
|
+
</div>
|
|
176
|
+
</Card>
|
|
177
|
+
);
|
|
178
|
+
};
|
package/template/src/main.tsx
CHANGED
|
@@ -5,24 +5,36 @@ import App from './App.tsx';
|
|
|
5
5
|
import { ThemeProvider, ToastProvider } from '@ramme-io/ui';
|
|
6
6
|
import { AuthProvider } from './features/auth/AuthContext.tsx';
|
|
7
7
|
import { MqttProvider } from './engine/runtime/MqttContext';
|
|
8
|
-
import "@ramme-io/ui/index.css";
|
|
8
|
+
import "@ramme-io/ui/index.css";
|
|
9
9
|
import './index.css';
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// 1. Data Seeder (Critical for Auth)
|
|
12
|
+
import { initializeDataLake } from './engine/runtime/data-seeder';
|
|
13
|
+
|
|
14
|
+
// 2. Manifest Provider (Critical for Builder Preview)
|
|
15
|
+
import { ManifestProvider } from './engine/runtime/ManifestContext';
|
|
16
|
+
|
|
17
|
+
// 3. AG Grid Enterprise
|
|
12
18
|
import 'ag-grid-enterprise';
|
|
13
19
|
|
|
20
|
+
// Initialize mock data immediately on boot
|
|
21
|
+
initializeDataLake();
|
|
22
|
+
|
|
14
23
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
15
24
|
<React.StrictMode>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
{/* 🛡️ KEEPS THE BRIDGE ALIVE: Wraps the entire app */}
|
|
26
|
+
<ManifestProvider>
|
|
27
|
+
<BrowserRouter>
|
|
28
|
+
<ThemeProvider>
|
|
29
|
+
<ToastProvider>
|
|
30
|
+
<AuthProvider>
|
|
31
|
+
<MqttProvider>
|
|
32
|
+
<App />
|
|
33
|
+
</MqttProvider>
|
|
34
|
+
</AuthProvider>
|
|
35
|
+
</ToastProvider>
|
|
36
|
+
</ThemeProvider>
|
|
37
|
+
</BrowserRouter>
|
|
38
|
+
</ManifestProvider>
|
|
27
39
|
</React.StrictMode>,
|
|
28
40
|
);
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
Sidebar,
|
|
5
5
|
type SidebarItem,
|
|
6
6
|
ChatFAB,
|
|
7
|
-
type IconName,
|
|
8
|
-
} from '@ramme-io/ui';
|
|
7
|
+
type IconName,
|
|
8
|
+
} from '@ramme-io/ui';
|
|
9
9
|
|
|
10
10
|
import { dashboardSitemap } from './dashboard.sitemap';
|
|
11
11
|
import { SitemapProvider } from '../../engine/runtime/SitemapContext';
|
|
@@ -14,7 +14,9 @@ import AppHeader from '../../components/AppHeader';
|
|
|
14
14
|
import { AIChatWidget } from '../../components/AIChatWidget';
|
|
15
15
|
import { useWorkflowEngine } from '../../engine/runtime/useWorkflowEngine';
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// ✅ 1. IMPORT THE HOOK
|
|
18
|
+
import { useDynamicSitemap } from '../../engine/runtime/useDynamicSitemap';
|
|
19
|
+
|
|
18
20
|
const DashboardLayout: React.FC = () => {
|
|
19
21
|
const navigate = useNavigate();
|
|
20
22
|
const location = useLocation();
|
|
@@ -22,39 +24,39 @@ const DashboardLayout: React.FC = () => {
|
|
|
22
24
|
|
|
23
25
|
useWorkflowEngine();
|
|
24
26
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
+
// ✅ 2. GET LIVE DATA (Merges Static + Builder Data)
|
|
28
|
+
const liveSitemap = useDynamicSitemap(dashboardSitemap);
|
|
29
|
+
|
|
30
|
+
// ✅ 3. BUILD SIDEBAR FROM LIVE DATA
|
|
27
31
|
const sidebarItems: SidebarItem[] = useMemo(() => {
|
|
28
|
-
|
|
32
|
+
// We map over 'liveSitemap' instead of 'dashboardSitemap'
|
|
33
|
+
return liveSitemap.map((route) => ({
|
|
29
34
|
id: route.id,
|
|
30
35
|
label: route.title,
|
|
31
|
-
icon: route.icon as IconName,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// If you need nested items, we would flatten them here or update Sidebar.tsx later.
|
|
36
|
+
icon: route.icon as IconName,
|
|
37
|
+
// Handle the path logic safely
|
|
38
|
+
href: route.path.startsWith('/') ? route.path : `/dashboard/${route.path}`,
|
|
35
39
|
}));
|
|
36
|
-
}, []);
|
|
40
|
+
}, [liveSitemap]); // Re-run whenever the Builder sends an update
|
|
37
41
|
|
|
38
|
-
//
|
|
42
|
+
// 4. Determine Active Item
|
|
39
43
|
const activeItemId = useMemo(() => {
|
|
40
|
-
// Find the item whose href matches the start of the current path
|
|
41
44
|
const active = sidebarItems.find(item =>
|
|
42
45
|
item.href !== '/dashboard' && location.pathname.startsWith(item.href!)
|
|
43
46
|
);
|
|
44
|
-
// Default to dashboard (first item) if no specific match
|
|
45
47
|
return active?.id || sidebarItems[0]?.id;
|
|
46
48
|
}, [location.pathname, sidebarItems]);
|
|
47
49
|
|
|
48
50
|
return (
|
|
49
|
-
|
|
51
|
+
// ✅ 5. PASS LIVE SITEMAP TO CONTEXT (For Breadcrumbs/Titles)
|
|
52
|
+
<SitemapProvider value={liveSitemap}>
|
|
50
53
|
<PageTitleUpdater />
|
|
51
54
|
|
|
52
|
-
{/* 3. New Layout Structure (No Provider needed) */}
|
|
53
55
|
<div className="flex h-screen bg-background text-foreground relative">
|
|
54
56
|
|
|
55
57
|
<Sidebar
|
|
56
58
|
className="relative border-border"
|
|
57
|
-
items={sidebarItems}
|
|
59
|
+
items={sidebarItems} // Now contains your new page!
|
|
58
60
|
activeItemId={activeItemId}
|
|
59
61
|
onNavigate={(item) => {
|
|
60
62
|
if (item.href) navigate(item.href);
|
|
@@ -78,7 +80,6 @@ const DashboardLayout: React.FC = () => {
|
|
|
78
80
|
</main>
|
|
79
81
|
</div>
|
|
80
82
|
|
|
81
|
-
{/* --- AI COPILOT SECTION --- */}
|
|
82
83
|
{isChatOpen && (
|
|
83
84
|
<AIChatWidget onClose={() => setIsChatOpen(false)} />
|
|
84
85
|
)}
|
|
@@ -18,14 +18,7 @@ export const dashboardSitemap: SitemapEntry[] = [
|
|
|
18
18
|
icon: 'rocket',
|
|
19
19
|
component: Welcome,
|
|
20
20
|
},
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
id: 'overview',
|
|
24
|
-
path: 'overview',
|
|
25
|
-
title: 'Overview',
|
|
26
|
-
icon: 'layout-dashboard',
|
|
27
|
-
component: OverviewPage,
|
|
28
|
-
},
|
|
21
|
+
|
|
29
22
|
// 3. ✅ The User Management Module
|
|
30
23
|
{
|
|
31
24
|
id: 'users',
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
|
|
3
|
-
// This finds the absolute path to the @ramme-io/ui package's directory
|
|
4
|
-
const rammeUiPath = path.dirname(require.resolve('@ramme-io/ui/package.json'));
|
|
5
|
-
|
|
6
1
|
/** @type {import('tailwindcss').Config} */
|
|
7
2
|
module.exports = {
|
|
8
3
|
presets: [
|
|
9
|
-
//
|
|
10
|
-
require(
|
|
4
|
+
// ✅ STANDARD: Directly require the preset from the package
|
|
5
|
+
require('@ramme-io/ui/tailwind.preset.js')
|
|
11
6
|
],
|
|
12
7
|
content: [
|
|
13
8
|
"./index.html",
|
|
14
9
|
"./src/**/*.{js,ts,jsx,tsx}",
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
|
|
11
|
+
// ✅ STANDARD: Point directly to node_modules relative to this config file
|
|
12
|
+
// This is the industry standard way (like how Flowbite or Shadcn work)
|
|
13
|
+
"./node_modules/@ramme-io/ui/dist/**/*.{js,ts,jsx,tsx}"
|
|
17
14
|
],
|
|
15
|
+
theme: {
|
|
16
|
+
extend: {},
|
|
17
|
+
},
|
|
18
|
+
plugins: [],
|
|
18
19
|
}
|
package/template/vite.config.ts
CHANGED
|
@@ -7,20 +7,6 @@ export default defineConfig({
|
|
|
7
7
|
resolve: {
|
|
8
8
|
alias: {
|
|
9
9
|
'@': path.resolve(__dirname, './src'),
|
|
10
|
-
|
|
11
|
-
// ✅ FIX: Go up 2 levels to find the sibling 'ramme-ui' folder
|
|
12
|
-
'@ramme-io/ui': path.resolve(__dirname, '../../ramme-ui/src'),
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
// ✅ CRITICAL: Allow Vite to serve files outside the current project folder
|
|
16
|
-
server: {
|
|
17
|
-
fs: {
|
|
18
|
-
allow: [
|
|
19
|
-
// Allow serving files from the project root
|
|
20
|
-
'.',
|
|
21
|
-
// Allow serving files from the sibling UI library
|
|
22
|
-
'../../ramme-ui',
|
|
23
|
-
],
|
|
24
10
|
},
|
|
25
11
|
},
|
|
26
12
|
});
|