@ramme-io/create-app 1.1.9 → 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.1.9",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-ramme-app": "./index.js"
@@ -14,10 +14,11 @@
14
14
  "build": "cd template && tsc && vite build",
15
15
  "preview": "cd template && vite preview",
16
16
  "pack-tarball": "npm pack",
17
- "bundle:ai": "repomix --ignore '**/*.lock,**/dist/**,**/template/dist/**,**/template/node_modules/**' --output 'ramme-starter-context.txt'"
17
+ "bundle:ai": "repomix --ignore '**/*.lock,**/dist/**,**/template/dist/**,**/template/node_modules/**' --output 'ramme-starter-context.txt'",
18
+ "zip:builder": "cd template && zip -r ../base.zip . -x \"node_modules/*\" \"dist/*\" \".DS_Store\" && mv ../base.zip ../../ramme-app-builder/public/base.zip && echo '✅ base.zip updated in Builder!'"
18
19
  },
19
20
  "dependencies": {
20
- "@ramme-io/ui": "^1.1.3",
21
+ "@ramme-io/ui": "^1.1.5",
21
22
  "ag-grid-community": "^34.1.2",
22
23
  "ag-grid-enterprise": "^34.1.2",
23
24
  "ag-grid-react": "^34.1.2",
package/template/pkg.json CHANGED
@@ -9,28 +9,31 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@ramme-io/ui": "^1.1.3",
12
+ "@ramme-io/ui": "^1.1.5",
13
13
  "mqtt": "^5.3.5",
14
14
  "zustand": "^4.5.0",
15
15
  "ag-grid-community": "^31.3.1",
16
- "ag-grid-enterprise": "^31.3.1",
17
- "ag-grid-react": "^31.3.1",
18
- "framer-motion": "^12.23.12",
19
- "fs-extra": "^11.3.1",
16
+ "ag-grid-enterprise": "^31.3.1",
17
+ "ag-grid-react": "^31.3.1",
18
+ "framer-motion": "^11.0.0",
19
+ "fs-extra": "^11.2.0",
20
20
  "react": "^18.2.0",
21
21
  "react-dom": "^18.2.0",
22
- "react-resizable-panels": "^3.0.4",
23
- "react-router-dom": "6.30.1"
22
+ "react-router-dom": "^6.22.0",
23
+ "react-resizable-panels": "^2.0.9",
24
+ "ai": "^3.0.0",
25
+ "@ai-sdk/google": "^0.0.10",
26
+ "zod": "^3.22.0"
24
27
  },
25
28
  "devDependencies": {
26
- "@types/node": "^20.14.2",
27
- "@types/react": "^18.2.66",
28
- "@types/react-dom": "^18.2.22",
29
- "@vitejs/plugin-react": "^4.2.1",
29
+ "@types/node": "^20.11.0",
30
+ "@types/react": "^18.2.0",
31
+ "@types/react-dom": "^18.2.0",
32
+ "@vitejs/plugin-react": "^4.2.0",
30
33
  "autoprefixer": "^10.4.19",
31
34
  "postcss": "^8.4.38",
32
- "tailwindcss": "^3.4.4",
33
- "typescript": "^5.2.2",
35
+ "tailwindcss": "^3.4.3",
36
+ "typescript": "^5.2.0",
34
37
  "vite": "^5.2.0"
35
38
  }
36
39
  }
@@ -1,42 +1,48 @@
1
1
  import { Routes, Route, Navigate } from 'react-router-dom';
2
2
  import { generateRoutes } from './core/route-generator';
3
+ import { useEffect } from 'react';
3
4
 
4
5
  // --- 1. IMPORT ALL THREE TEMPLATES ---
5
6
  import DashboardLayout from './templates/dashboard/DashboardLayout';
6
- import { dashboardSitemap as dashboardSitemap } from './templates/dashboard/dashboard.sitemap';
7
+ import { dashboardSitemap } from './templates/dashboard/dashboard.sitemap';
7
8
  import DocsLayout from './templates/docs/DocsLayout';
8
- import { docsSitemap as docsSitemap } from './templates/docs/docs.sitemap';
9
- import SettingsLayout from './templates/settings/SettingsLayout'; // <-- NEW
10
- import { settingsSitemap as settingsSitemap } from './templates/settings/settings.sitemap'; // <-- NEW
9
+ import { docsSitemap } from './templates/docs/docs.sitemap';
10
+ import SettingsLayout from './templates/settings/SettingsLayout';
11
+ import { settingsSitemap } from './templates/settings/settings.sitemap';
11
12
 
12
13
  // Other Imports
13
14
  import ProtectedRoute from './components/ProtectedRoute';
14
15
  import LoginPage from './pages/LoginPage';
15
16
  import NotFound from './pages/styleguide/NotFound';
16
17
 
18
+ // --- 2. IMPORT THE SEEDER ---
19
+ import { initializeDataLake } from './core/data-seeder';
20
+
17
21
  function App() {
22
+
23
+ // ✅ 3. TRIGGER DATA SEEDING ON MOUNT
24
+ useEffect(() => {
25
+ initializeDataLake();
26
+ }, []);
27
+
18
28
  return (
19
29
  <Routes>
20
30
  <Route path="/login" element={<LoginPage />} />
21
31
  <Route element={<ProtectedRoute />}>
22
- {/* Default redirect to the dashboard's root */}
23
- <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 />} />
24
34
 
25
35
  {/* Dashboard Template Routes */}
26
36
  <Route path="/dashboard/*" element={<DashboardLayout />}>
27
- {' '}
28
- {/* <-- Added /* */}
29
37
  {generateRoutes(dashboardSitemap)}
30
38
  </Route>
31
39
 
32
40
  {/* Docs Template Routes */}
33
41
  <Route path="/docs/*" element={<DocsLayout />}>
34
- {' '}
35
- {/* <-- Added /* */}
36
42
  {generateRoutes(docsSitemap)}
37
43
  </Route>
38
44
 
39
- {/* --- 2. ADD THE NEW SETTINGS LAYOUT ROUTE --- */}
45
+ {/* Settings Layout Route */}
40
46
  <Route path="/settings/*" element={<SettingsLayout />}>
41
47
  {generateRoutes(settingsSitemap)}
42
48
  </Route>
@@ -0,0 +1,224 @@
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';
12
+ import { getResourceMeta, getMockData } from '../data/mockData';
13
+ import { AutoForm } from '../components/AutoForm';
14
+ import { useCrudLocalStorage } from '../hooks/useCrudLocalStorage';
15
+
16
+ interface SmartTableProps {
17
+ dataId: string;
18
+ title?: string;
19
+ initialFilter?: Record<string, any>;
20
+ }
21
+
22
+ export const SmartTable: React.FC<SmartTableProps> = ({
23
+ dataId,
24
+ title,
25
+ initialFilter
26
+ }) => {
27
+ const { addToast } = useToast();
28
+
29
+ // 1. DATA KERNEL
30
+ const meta = getResourceMeta(dataId);
31
+ const seedData = useMemo(() => getMockData(dataId) || [], [dataId]);
32
+
33
+ // We use the CRUD hook to persist changes to localStorage
34
+ const {
35
+ data: rowData,
36
+ createItem,
37
+ updateItem,
38
+ deleteItem
39
+ } = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
40
+
41
+ // 2. UI STATE
42
+ const [isEditOpen, setIsEditOpen] = useState(false);
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('');
47
+
48
+ // 3. COLUMN DEFINITIONS
49
+ const columns = useMemo<ColDef[]>(() => {
50
+ if (!meta?.fields) return [];
51
+
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}`}>
81
+ {p.value}
82
+ </span>
83
+ );
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" />
106
+ </Button>
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());
122
+ }
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');
130
+ }
131
+ };
132
+
133
+ const handleSave = (record: any) => {
134
+ if (record.id && currentRecord?.id) {
135
+ updateItem(record);
136
+ addToast('Item updated', 'success');
137
+ } else {
138
+ const { id, ...newItem } = record;
139
+ createItem(newItem);
140
+ addToast('Item created', 'success');
141
+ }
142
+ setIsEditOpen(false);
143
+ };
144
+
145
+ // --- RENDER ---
146
+ return (
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 */}
179
+ <div className="flex items-center gap-2">
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>
194
+ </div>
195
+ </div>
196
+
197
+ {/* AG GRID */}
198
+ <div className="flex-1 w-full bg-card relative">
199
+ <DataTable
200
+ rowData={rowData}
201
+ columnDefs={columns}
202
+ onGridReady={onGridReady}
203
+ onSelectionChanged={onSelectionChanged}
204
+ rowSelection="multiple"
205
+ pagination={true}
206
+ paginationPageSize={10}
207
+ headerHeight={48}
208
+ rowHeight={48}
209
+ enableCellTextSelection={true}
210
+ />
211
+ </div>
212
+
213
+ {/* EDIT DRAWER */}
214
+ <AutoForm
215
+ isOpen={isEditOpen}
216
+ onClose={() => setIsEditOpen(false)}
217
+ onSubmit={handleSave}
218
+ title={meta?.name || 'Item'}
219
+ fields={meta?.fields || []}
220
+ initialData={currentRecord}
221
+ />
222
+ </Card>
223
+ );
224
+ };
@@ -0,0 +1,128 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Drawer, FormTemplate, Button, type FormField } from '@ramme-io/ui';
3
+
4
+ interface AutoFormProps {
5
+ isOpen: boolean;
6
+ onClose: () => void;
7
+ onSubmit: (data: any) => void;
8
+ title: string;
9
+ fields: any[]; // The raw metadata from app.manifest.ts
10
+ initialData?: any;
11
+ }
12
+
13
+ export const AutoForm: React.FC<AutoFormProps> = ({
14
+ isOpen,
15
+ onClose,
16
+ onSubmit,
17
+ title,
18
+ fields,
19
+ initialData
20
+ }) => {
21
+
22
+ // 1. Safe Data Handling
23
+ const safeData = initialData || {};
24
+ const isEditMode = !!safeData.id;
25
+
26
+ // 2. Schema-to-UI Mapping Engine
27
+ const formFields = useMemo<FormField[]>(() => {
28
+ return fields
29
+ // 🛑 RULE 1: Hide ID field during creation (System handles it)
30
+ .filter((f: any) => !(f.key === 'id' && !isEditMode))
31
+ .map((f: any) => {
32
+
33
+ const isIdField = f.key === 'id';
34
+
35
+ // Base Configuration for all fields
36
+ const baseConfig = {
37
+ name: f.key,
38
+ label: f.label,
39
+ required: f.required,
40
+ placeholder: f.description || `Enter ${f.label.toLowerCase()}`,
41
+ // Pre-fill data if editing, or use default from schema
42
+ value: safeData[f.key] !== undefined ? safeData[f.key] : (f.defaultValue || ''),
43
+ colSpan: 1,
44
+ // 🛑 RULE 2: If it's the ID field, force it to be disabled (View Only)
45
+ disabled: isIdField
46
+ };
47
+
48
+ // Intelligent Type Switching
49
+ switch (f.type) {
50
+ case 'number':
51
+ case 'integer':
52
+ case 'currency':
53
+ return {
54
+ ...baseConfig,
55
+ type: 'number',
56
+ value: Number(baseConfig.value) || 0
57
+ };
58
+
59
+ case 'boolean':
60
+ return {
61
+ ...baseConfig,
62
+ type: 'toggle',
63
+ checked: !!baseConfig.value
64
+ };
65
+
66
+ case 'date':
67
+ return {
68
+ ...baseConfig,
69
+ type: 'datepicker',
70
+ value: baseConfig.value ? new Date(baseConfig.value) : null
71
+ };
72
+
73
+ case 'status':
74
+ return {
75
+ ...baseConfig,
76
+ type: 'select',
77
+ options: [
78
+ { value: 'Active', label: 'Active' },
79
+ { value: 'Pending', label: 'Pending' },
80
+ { value: 'Inactive', label: 'Inactive' },
81
+ { value: 'Archived', label: 'Archived' }
82
+ ]
83
+ };
84
+
85
+ case 'textarea':
86
+ return {
87
+ ...baseConfig,
88
+ type: 'textarea',
89
+ colSpan: 2
90
+ };
91
+
92
+ case 'email':
93
+ return { ...baseConfig, type: 'email' };
94
+
95
+ default:
96
+ return { ...baseConfig, type: 'text' };
97
+ }
98
+ });
99
+ }, [fields, safeData, isEditMode]);
100
+
101
+ return (
102
+ <Drawer
103
+ isOpen={isOpen}
104
+ onClose={onClose}
105
+ title={`${isEditMode ? 'Edit' : 'New'} ${title}`}
106
+ size="500px"
107
+ >
108
+ <div className="p-6">
109
+ <FormTemplate
110
+ fields={formFields}
111
+ onSubmit={(formData) => {
112
+ // Merge original ID to ensure we update the correct record
113
+ onSubmit({ ...safeData, ...formData });
114
+ }}
115
+ >
116
+ <div className="flex justify-end gap-2 mt-8 pt-4 border-t border-border">
117
+ <Button variant="outline" onClick={onClose} type="button">
118
+ Cancel
119
+ </Button>
120
+ <Button type="submit" variant="primary">
121
+ {isEditMode ? 'Save Changes' : 'Create Record'}
122
+ </Button>
123
+ </div>
124
+ </FormTemplate>
125
+ </div>
126
+ </Drawer>
127
+ );
128
+ };
@@ -1,52 +1,58 @@
1
1
  import React from 'react';
2
2
  import { getComponent } from '../core/component-registry';
3
- import { useSignal } from '../hooks/useSignal';
4
- import { getMockData } from '../data/mockData';
3
+ // @ts-ignore
4
+ import { useGeneratedSignals } from '../generated/hooks';
5
+ import { getMockData } from '../data/mockData';
5
6
 
6
7
  const mapSignalStatus = (status: string): string => {
7
8
  switch (status) {
8
9
  case 'fresh': return 'online';
9
10
  case 'stale': return 'warning';
10
11
  case 'disconnected': return 'offline';
11
- case 'error': return 'error';
12
+ case 'error': return 'error';
12
13
  default: return 'offline';
13
14
  }
14
15
  };
15
16
 
16
- interface DynamicBlockProps {
17
- block: {
18
- id: string;
19
- type: string;
20
- props: Record<string, any>;
21
- };
22
- }
23
-
24
- export const DynamicBlock: React.FC<DynamicBlockProps> = ({ block }) => {
17
+ export const DynamicBlock: React.FC<any> = ({ block }) => {
25
18
  const Component = getComponent(block.type);
26
- const { signalId, dataId, ...staticProps } = block.props;
27
-
28
- // 1. Resolve Data
29
- const resolvedData = dataId ? getMockData(dataId) : staticProps.data;
19
+ const signals = useGeneratedSignals();
30
20
 
31
- // 2. Signal Wiring
32
- const signalState = useSignal(signalId || '');
33
-
34
- // 3. Merge Props
21
+ const { signalId, dataId, ...staticProps } = block.props;
22
+
35
23
  const dynamicProps: Record<string, any> = {
36
24
  ...staticProps,
37
- // FIX: Pass data as BOTH 'data' (Charts) and 'rowData' (Tables)
38
- data: resolvedData || [],
39
- rowData: resolvedData || [],
25
+ dataId,
26
+ signalId
40
27
  };
41
28
 
42
- // 4. Inject Signal Data
43
- if (signalId && signalState) {
44
- dynamicProps.value = `${signalState.value}${signalState.unit || ''}`;
45
- if (signalState.status) {
46
- dynamicProps.status = mapSignalStatus(signalState.status);
47
- if (signalState.status === 'error') {
48
- dynamicProps.variant = 'destructive';
49
- }
29
+ // --- DATA INJECTION ---
30
+ if (dataId) {
31
+ const resolvedData = getMockData(dataId);
32
+ dynamicProps.data = resolvedData || [];
33
+ dynamicProps.rowData = resolvedData || [];
34
+ }
35
+
36
+ // --- SIGNAL INJECTION ---
37
+ if (signalId && signals && signalId in signals) {
38
+ // @ts-ignore
39
+ const signalState = signals[signalId];
40
+
41
+ if (signalState) {
42
+ // ✅ FIX: Handle both Raw Values (Legacy) and Signal Objects (New)
43
+ // If it's an object with a 'value' property, use that. Otherwise use it directly.
44
+ const rawValue = (typeof signalState === 'object' && 'value' in signalState)
45
+ ? signalState.value
46
+ : signalState;
47
+
48
+ const status = (typeof signalState === 'object' && 'status' in signalState)
49
+ ? signalState.status
50
+ : 'fresh'; // Default for raw values
51
+
52
+ dynamicProps.value = typeof rawValue === 'number' ? rawValue : String(rawValue);
53
+ dynamicProps.status = mapSignalStatus(status);
54
+ } else {
55
+ dynamicProps.status = mapSignalStatus('disconnected');
50
56
  }
51
57
  }
52
58