@ramme-io/create-app 1.1.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,73 +1,83 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { useMqtt } from '../contexts/MqttContext';
2
3
  import type { Signal } from '../types/signal';
3
4
 
4
- /**
5
- * Configuration for the Mock Generator.
6
- * This allows us to simulate specific hardware constraints.
7
- */
8
5
  interface SignalConfig<T> {
9
6
  initialValue?: T;
10
- min?: number; // Prevent drifting too low
11
- max?: number; // Prevent drifting too high
12
- interval?: number; // Update speed (ms)
13
- unit?: string; // e.g., "°F", "%", "RPM"
7
+ min?: number;
8
+ max?: number;
9
+ interval?: number; // Mock mode only
10
+ unit?: string;
11
+ topic?: string; // Real mode only
14
12
  }
15
13
 
16
- /**
17
- * The "Universal Socket."
18
- * UI Components use this hook to subscribe to data.
19
- * Currently running in "Synthetic Mode" (Mock) with bounded randomization.
20
- */
21
14
  export function useSignal<T = any>(signalId: string, config: SignalConfig<T> = {}): Signal<T> {
22
- // Defaults
23
15
  const {
24
16
  initialValue,
25
17
  min = -Infinity,
26
18
  max = Infinity,
27
19
  interval = 2000,
28
- unit
20
+ unit,
21
+ topic
29
22
  } = config;
30
23
 
31
- // 1. Initialize
24
+ const { subscribe, unsubscribe, lastMessage, isConnected } = useMqtt();
25
+
32
26
  const [signal, setSignal] = useState<Signal<T>>({
33
27
  id: signalId,
34
28
  value: initialValue as T,
35
29
  unit: unit,
36
30
  timestamp: Date.now(),
37
31
  status: 'fresh',
32
+ max: max
38
33
  });
39
34
 
35
+ // --- REAL MODE: MQTT ---
36
+ useEffect(() => {
37
+ if (!topic || !isConnected) return;
38
+ subscribe(topic);
39
+ return () => unsubscribe(topic);
40
+ }, [topic, isConnected, subscribe, unsubscribe]);
41
+
40
42
  useEffect(() => {
41
- // 2. SIMULATION: Generate synthetic data updates
43
+ if (!topic || !lastMessage[topic]) return;
44
+
45
+ const rawValue = lastMessage[topic];
46
+ let parsedValue: any = rawValue;
47
+
48
+ // Auto-parse numbers and booleans
49
+ if (!isNaN(Number(rawValue))) parsedValue = Number(rawValue);
50
+ else if (rawValue.toLowerCase() === 'true' || rawValue === 'on') parsedValue = true;
51
+ else if (rawValue.toLowerCase() === 'false' || rawValue === 'off') parsedValue = false;
52
+
53
+ setSignal(prev => ({
54
+ ...prev,
55
+ value: parsedValue,
56
+ timestamp: Date.now(),
57
+ status: 'fresh'
58
+ }));
59
+ }, [lastMessage, topic]);
60
+
61
+ // --- MOCK MODE: SIMULATION ---
62
+ useEffect(() => {
63
+ if (topic) return; // Disable mock if topic exists
64
+
42
65
  const timer = setInterval(() => {
43
66
  setSignal(prev => {
44
67
  let newValue: any = prev.value;
45
-
46
- // Only apply math if it's a number
47
68
  if (typeof prev.value === 'number') {
48
- // Generate drift
49
- const variance = (Math.random() - 0.5) * 2; // +/- 1
69
+ const variance = (Math.random() - 0.5) * 2;
50
70
  let nextNum = prev.value + variance;
51
-
52
- // 🛡️ CLAMPING: Apply the physical bounds
53
71
  if (min !== undefined) nextNum = Math.max(min, nextNum);
54
72
  if (max !== undefined) nextNum = Math.min(max, nextNum);
55
-
56
73
  newValue = Number(nextNum.toFixed(1));
57
74
  }
58
-
59
- return {
60
- ...prev,
61
- value: newValue,
62
- timestamp: Date.now(),
63
- status: 'fresh',
64
- unit: unit // Ensure unit persists
65
- };
75
+ return { ...prev, value: newValue, timestamp: Date.now(), status: 'fresh' };
66
76
  });
67
77
  }, interval);
68
78
 
69
79
  return () => clearInterval(timer);
70
- }, [signalId, min, max, interval, unit]);
80
+ }, [topic, min, max, interval]);
71
81
 
72
82
  return signal;
73
83
  }
@@ -0,0 +1,6 @@
1
+ // Placeholder for the Generated Engine
2
+ export const useWorkflowEngine = () => {
3
+ // This will be overwritten by the Builder at runtime
4
+ console.log(" [Mock] Workflow Engine Mounted");
5
+ return {};
6
+ };
@@ -1,105 +1,58 @@
1
1
  import React from 'react';
2
- import { PageHeader, ToggleSwitch, Button, Icon } from '@ramme-io/ui';
3
-
4
- // --- IMPORTS ---
5
- import { getComponent } from '../core/component-registry';
6
- import { appManifest } from '../config/app.manifest';
7
- import { useGeneratedSignals } from '../generated/hooks';
8
- import type { EntityDefinition } from '../types/schema';
9
- import { useAction } from '../hooks/useAction';
10
-
11
- // --- DEV TOOLS (Restored) ---
12
- import { useDevTools } from '../hooks/useDevTools';
13
- import { GhostOverlay } from '../components/dev/GhostOverlay';
2
+ import { useLocation } from 'react-router-dom';
3
+ import { PageHeader, Alert } from '@ramme-io/ui';
4
+ import { appManifest } from '../config/app.manifest';
5
+ import { DynamicBlock } from '../components/DynamicBlock';
14
6
 
15
7
  const Dashboard: React.FC = () => {
16
- // 1. Initialize State Machine
17
- const signals = useGeneratedSignals();
18
- const { meta, domain } = appManifest;
8
+ const location = useLocation();
19
9
 
20
- // 2. Initialize DevTools
21
- const { isGhostMode, toggleGhostMode } = useDevTools();
22
- const { sendAction } = useAction();
10
+ // 1. Determine current slug from URL
11
+ const pathParts = location.pathname.split('/').filter(Boolean);
12
+ // If path is root or /dashboard, slug is 'dashboard', else take the last part
13
+ const currentSlug = pathParts.length > 1 ? pathParts[pathParts.length - 1] : 'dashboard';
14
+
15
+ // 2. Find the matching Page Definition
16
+ const pageDef = appManifest.pages?.find(p => p.slug === currentSlug)
17
+ || appManifest.pages?.[0];
18
+
19
+ if (!pageDef) {
20
+ return (
21
+ <div className="p-8">
22
+ <Alert variant="warning" title="Page Not Found">
23
+ Could not find a definition for slug: <code>{currentSlug}</code>
24
+ </Alert>
25
+ </div>
26
+ );
27
+ }
23
28
 
24
29
  return (
25
30
  <div className="space-y-8 relative">
26
31
  <PageHeader
27
- title={meta.name || "Command Center"}
28
- description={meta.description || "Real-time device monitoring."}
29
- actions={
30
- // 3. Restore the Toggle Button
31
- <Button
32
- variant={isGhostMode ? 'accent' : 'outline'}
33
- size="sm"
34
- onClick={toggleGhostMode}
35
- title="Toggle Ghost Mode (Ctrl+Shift+G)"
36
- >
37
- <Icon name={isGhostMode ? 'eye' : 'eye-off'} className="mr-2" />
38
- {isGhostMode ? 'Ghost Mode: ON' : 'Dev Tools'}
39
- </Button>
40
- }
32
+ title={pageDef.title}
33
+ description={pageDef.description}
41
34
  />
42
35
 
43
- {/* --- DYNAMIC RUNTIME ENGINE --- */}
44
- <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
45
-
46
- {domain.entities.map((entity: EntityDefinition) => {
47
- // A. Resolve Component & Data
48
- const componentType = entity.ui?.dashboardComponent || 'DeviceCard';
49
- const Component = getComponent(componentType);
36
+ {pageDef.sections.map((section) => (
37
+ <div key={section.id} className="relative">
38
+ {section.title && (
39
+ <h3 className="text-lg font-semibold mb-4 text-foreground">
40
+ {section.title}
41
+ </h3>
42
+ )}
50
43
 
51
- const primarySignalId = entity.signals[0];
52
- const signal = primarySignalId ? signals[primarySignalId as keyof typeof signals] : null;
53
-
54
- // B. Prepare Props
55
- const dynamicProps: any = {
56
- title: entity.name,
57
- description: entity.description || `ID: ${entity.id}`,
58
- icon: entity.ui?.icon || 'activity',
59
- status: 'offline',
60
- };
61
-
62
- // C. Inject Signal Data
63
- if (signal) {
64
- dynamicProps.value = `${signal.value} ${signal.unit || ''}`;
65
- dynamicProps.status = 'active';
66
-
67
- if (componentType === 'ToggleCard') {
68
- dynamicProps.children = (
69
- <div className="flex items-center justify-between mt-2">
70
- <span className="text-sm text-muted-foreground">Active</span>
71
- <ToggleSwitch
72
- label="Toggle"
73
- checked={(signal.value as any) === true || signal.value === 'true' || signal.value === 1}
74
- // THE UPDATE:
75
- onChange={(val) => sendAction(entity.id, val)}
76
- />
77
- </div>
78
- );
79
- delete dynamicProps.value;
80
- }
81
- }
82
-
83
- return (
84
- // 4. Restore the GhostOverlay Wrapper
85
- <GhostOverlay
86
- key={entity.id}
87
- isActive={isGhostMode}
88
- componentId={entity.id}
89
- componentType={componentType}
90
- signalId={primarySignalId}
91
- >
92
- <Component {...dynamicProps} />
93
- </GhostOverlay>
94
- );
95
- })}
96
- </div>
97
-
98
- {domain.entities.length === 0 && (
99
- <div className="p-12 text-center border-2 border-dashed border-slate-200 rounded-lg text-slate-400">
100
- <p>No entities defined.</p>
44
+ <div
45
+ className="grid gap-6"
46
+ style={{
47
+ gridTemplateColumns: `repeat(${section.layout?.columns || 3}, minmax(300px, 1fr))`
48
+ }}
49
+ >
50
+ {section.blocks.map((block) => (
51
+ <DynamicBlock key={block.id} block={block} />
52
+ ))}
53
+ </div>
101
54
  </div>
102
- )}
55
+ ))}
103
56
  </div>
104
57
  );
105
58
  };
@@ -0,0 +1,95 @@
1
+ import React from 'react';
2
+ import { PageHeader, Alert, Button, Icon } from '@ramme-io/ui';
3
+ import { appManifest } from '../config/app.manifest';
4
+ // Reuse the GhostOverlay for structure visualization
5
+ import { GhostOverlay } from '../components/dev/GhostOverlay';
6
+ // Import the Worker Bee
7
+ import { DynamicBlock } from '../components/DynamicBlock';
8
+ // Import the Hook to control the overlay
9
+ import { useDevTools } from '../hooks/useDevTools';
10
+
11
+ interface DynamicPageProps {
12
+ pageId: string;
13
+ }
14
+
15
+ export const DynamicPage: React.FC<DynamicPageProps> = ({ pageId }) => {
16
+ // 1. Initialize DevTools
17
+ const { isGhostMode, toggleGhostMode } = useDevTools();
18
+
19
+ // 2. Look up the page definition
20
+ const pageConfig = (appManifest as any).pages?.find((p: any) => p.id === pageId);
21
+
22
+ if (!pageConfig) {
23
+ return (
24
+ <div className="p-8">
25
+ <Alert variant="danger" title="Page Not Found">
26
+ The manifest does not contain a page with ID: <code>{pageId}</code>.
27
+ </Alert>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <div className="space-y-8 fade-in p-6">
34
+ <PageHeader
35
+ title={pageConfig.title}
36
+ description={pageConfig.description}
37
+ actions={
38
+ <Button
39
+ variant={isGhostMode ? 'accent' : 'outline'}
40
+ size="sm"
41
+ onClick={toggleGhostMode}
42
+ title="Toggle Ghost Mode (Ctrl+Shift+G)"
43
+ >
44
+ <Icon name={isGhostMode ? 'eye' : 'eye-off'} className="mr-2" />
45
+ {isGhostMode ? 'Ghost Mode: ON' : 'Dev Tools'}
46
+ </Button>
47
+ }
48
+ />
49
+
50
+ {/* 4. Render Sections */}
51
+ {pageConfig.sections.map((section: any) => (
52
+ <div key={section.id} className="space-y-4">
53
+ {section.title && <h3 className="text-xl font-semibold">{section.title}</h3>}
54
+
55
+ <div
56
+ className="grid gap-6"
57
+ style={{
58
+ // ⚡️ IMPROVEMENT: Default to 1 column (Full Width) instead of 3.
59
+ // This fixes the "narrow table" issue immediately.
60
+ gridTemplateColumns: `repeat(${section.layout?.columns || 1}, minmax(0, 1fr))`
61
+ }}
62
+ >
63
+ {section.blocks.map((block: any) => {
64
+ // ⚡️ IMPROVEMENT: Calculate Spanning
65
+ // Allows a block to stretch across multiple columns if needed.
66
+ const colSpan = block.layout?.colSpan || 1;
67
+ const rowSpan = block.layout?.rowSpan || 1;
68
+
69
+ return (
70
+ <div
71
+ key={block.id}
72
+ style={{
73
+ gridColumn: `span ${colSpan}`,
74
+ gridRow: `span ${rowSpan}`
75
+ }}
76
+ >
77
+ <GhostOverlay
78
+ isActive={isGhostMode}
79
+ componentId={block.id}
80
+ componentType={block.type}
81
+ signalId={block.props.signalId}
82
+ >
83
+ <DynamicBlock block={block} />
84
+ </GhostOverlay>
85
+ </div>
86
+ );
87
+ })}
88
+ </div>
89
+ </div>
90
+ ))}
91
+ </div>
92
+ );
93
+ };
94
+
95
+ export default DynamicPage;
@@ -18,6 +18,7 @@ import { SitemapProvider } from '../../contexts/SitemapContext';
18
18
  import PageTitleUpdater from '../../components/PageTitleUpdater';
19
19
  import AppHeader from '../../components/AppHeader';
20
20
  import { AIChatWidget } from '../../components/AIChatWidget'; // <-- NEW: Import Widget
21
+ import { useWorkflowEngine } from '../../hooks/useWorkflowEngine';
21
22
 
22
23
  // NavLink wrapper - Correct
23
24
  const SidebarNavLink = React.forwardRef<HTMLAnchorElement, any>(
@@ -87,6 +88,7 @@ const AppSidebarContent: React.FC = () => {
87
88
  const DashboardLayout: React.FC = () => {
88
89
  // 1. STATE: Track if the chat window is open
89
90
  const [isChatOpen, setIsChatOpen] = useState(false);
91
+ useWorkflowEngine();
90
92
 
91
93
  return (
92
94
  <SitemapProvider value={dashboardSitemap}>
@@ -1,83 +1,33 @@
1
1
  import { type SitemapEntry } from '../../core/sitemap-entry';
2
-
3
- // Component Imports
2
+ import { appManifest } from '../../config/app.manifest';
4
3
  import Dashboard from '../../pages/Dashboard';
5
4
  import AiChat from '../../pages/AiChat';
6
- import DataGridPage from '../../pages/DataGridPage';
7
- import Styleguide from '../../pages/styleguide/Styleguide';
8
- import DataLayout from '../../layouts/DataLayout';
9
-
10
- // Style Guide Section Imports
11
- import TemplatesSection from '../../pages/styleguide/sections/templates/TemplatesSection';
12
- import LayoutSection from '../../pages/styleguide/sections/layout/LayoutSection';
13
- import ThemingSection from '../../pages/styleguide/sections/theming/ThemingSection';
14
- import NavigationSection from '../../pages/styleguide/sections/navigation/NavigationSection';
15
- import TablesSection from '../../pages/styleguide/sections/tables/TablesSection';
16
- import ChartsSection from '../../pages/styleguide/sections/charts/ChartsSection';
17
- import ElementsSection from '../../pages/styleguide/sections/elements/ElementsSection';
18
- import FormsSection from '../../pages/styleguide/sections/forms/FormsSection';
19
- import FeedbackSection from '../../pages/styleguide/sections/feedback/FeedbackSection';
20
- import UtilitiesSection from '../../pages/styleguide/sections/utilities/UtilitiesSection';
21
- import ColorsSection from '../../pages/styleguide/sections/colors/ColorsSection';
22
- import IconsSection from '../../pages/styleguide/sections/icons/IconsSection';
23
5
 
24
- // --- ADD THIS IMPORT ---
25
- import ItemSelectorPage from '../../pages/prototypes/ItemSelectorPage';
6
+ export const dashboardSitemap: SitemapEntry[] = [];
26
7
 
8
+ // A. Dynamic Pages from Manifest
9
+ if (appManifest.pages) {
10
+ appManifest.pages.forEach(page => {
11
+ const isDashboard = page.slug === 'dashboard';
12
+
13
+ dashboardSitemap.push({
14
+ id: page.id,
15
+ title: page.title,
16
+ // Map root to Dashboard, others to their slug
17
+ path: isDashboard ? '' : page.slug,
18
+ icon: isDashboard ? 'layout-dashboard' : 'file-text',
19
+ component: Dashboard, // Map everything to the Universal Renderer
20
+ });
21
+ });
22
+ }
27
23
 
28
- export const dashboardSitemap: SitemapEntry[] = [
29
- {
30
- id: 'dashboard',
31
- path: '',
32
- title: 'Home Overview',
33
- icon: 'layout-dashboard',
34
- component: Dashboard,
35
- },
36
-
37
- // --- ADD THIS NEW SITEMAP ENTRY ---
38
- {
39
- id: 'entity-selector-prototype',
40
- path: 'prototypes/entity-selector',
41
- title: 'Entity Prototype',
42
- icon: 'beaker',
43
- component: ItemSelectorPage,
44
- },
45
- {
24
+ // B. Dynamic Modules
25
+ if (appManifest.modules?.includes('ai-chat')) {
26
+ dashboardSitemap.push({
46
27
  id: 'ai-chat',
47
28
  path: 'ai-chat',
48
- title: 'AI Chat',
29
+ title: 'AI Assistant',
49
30
  icon: 'bot',
50
31
  component: AiChat,
51
- },
52
- {
53
- id: 'data',
54
- path: 'data',
55
- title: 'Data',
56
- icon: 'database',
57
- component: DataLayout,
58
- children: [
59
- { id: 'users', path: 'users', title: 'Users', component: DataGridPage, icon: 'table' }
60
- ],
61
- },
62
- {
63
- id: 'styleguide',
64
- path: 'styleguide',
65
- title: 'Style Guide',
66
- icon: 'palette',
67
- component: Styleguide,
68
- children: [
69
- { id: 'templates', path: 'templates', title: 'Templates', component: TemplatesSection },
70
- { id: 'layout', path: 'layout', title: 'Layout', component: LayoutSection },
71
- { id: 'theming', path: 'theming', title: 'Theming', component: ThemingSection },
72
- { id: 'navigation', path: 'navigation', title: 'Navigation', component: NavigationSection },
73
- { id: 'tables', path: 'tables', title: 'Tables', component: TablesSection },
74
- { id: 'charts', path: 'charts', title: 'Charts', component: ChartsSection },
75
- { id: 'elements', path: 'elements', title: 'Elements', component: ElementsSection },
76
- { id: 'forms', path: 'forms', title: 'Forms', component: FormsSection },
77
- { id: 'feedback', path: 'feedback', title: 'Feedback', component: FeedbackSection },
78
- { id: 'utilities', path: 'utilities', title: 'Utilities', component: UtilitiesSection },
79
- { id: 'colors', path: 'colors', title: 'Colors', component: ColorsSection },
80
- { id: 'icons', path: 'icons', title: 'Icons', component: IconsSection },
81
- ],
82
- },
83
- ];
32
+ });
33
+ }
@@ -1,85 +1,138 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  // ------------------------------------------------------------------
4
- // 1. Signal Schema
4
+ // 1. DATA RESOURCE DEFINITIONS (SaaS Layer)
5
5
  // ------------------------------------------------------------------
6
+
7
+ export const FieldSchema = z.object({
8
+ key: z.string(),
9
+ label: z.string(),
10
+ type: z.enum(['text', 'number', 'currency', 'date', 'boolean', 'status', 'email', 'image', 'textarea']),
11
+ required: z.boolean().optional(),
12
+ description: z.string().optional(),
13
+ defaultValue: z.any().optional(),
14
+ });
15
+ export type FieldDefinition = z.infer<typeof FieldSchema>;
16
+
17
+ export const ResourceSchema = z.object({
18
+ id: z.string(),
19
+ name: z.string(),
20
+ fields: z.array(FieldSchema),
21
+ defaultView: z.enum(['table', 'grid', 'list']).optional(),
22
+ features: z.object({
23
+ searchable: z.boolean().optional(),
24
+ creatable: z.boolean().optional(),
25
+ editable: z.boolean().optional(),
26
+ deletable: z.boolean().optional(),
27
+ exportable: z.boolean().optional(),
28
+ }).optional(),
29
+ });
30
+ export type ResourceDefinition = z.infer<typeof ResourceSchema>;
31
+
32
+
33
+ // ------------------------------------------------------------------
34
+ // 2. SIGNAL & IOT DEFINITIONS (Physical Layer)
35
+ // ------------------------------------------------------------------
36
+
6
37
  export const SignalSchema = z.object({
7
38
  id: z.string().min(1, "Signal ID is required"),
8
39
  label: z.string(),
9
40
  description: z.string().optional(),
10
-
11
- // Classification
12
41
  kind: z.enum(['sensor', 'actuator', 'setpoint', 'metric', 'status', 'kpi']),
13
-
14
- // Data Source Configuration
15
42
  source: z.enum(['mock', 'mqtt', 'http', 'derived', 'local']),
16
43
 
17
- // Protocol-Specific Config
18
- topic: z.string().optional(), // For MQTT
19
- endpoint: z.string().optional(), // For HTTP
20
- jsonPath: z.string().optional(), // For parsing nested API responses
44
+ // Connectivity
45
+ topic: z.string().optional(),
46
+ endpoint: z.string().optional(),
47
+ jsonPath: z.string().optional(),
21
48
  refreshRate: z.number().optional().default(1000),
22
49
 
23
- // Value Configuration
50
+ // Values
24
51
  defaultValue: z.any().optional(),
25
52
  unit: z.string().optional(),
26
-
27
- // Validation
28
53
  min: z.number().optional(),
29
54
  max: z.number().optional(),
30
55
  });
31
-
32
56
  export type SignalDefinition = z.infer<typeof SignalSchema>;
33
57
 
34
-
35
- // ------------------------------------------------------------------
36
- // 2. Entity Schema
37
- // ------------------------------------------------------------------
38
58
  export const EntitySchema = z.object({
39
59
  id: z.string(),
40
60
  name: z.string(),
41
61
  description: z.string().optional(),
42
-
43
- // Taxonomy
44
62
  type: z.string(),
45
63
  category: z.string().default('logical'),
46
-
47
- // Linkage
48
64
  signals: z.array(z.string()),
49
-
50
- // UI Hints
51
65
  ui: z.object({
52
66
  icon: z.string().optional(),
53
67
  color: z.string().optional(),
54
68
  dashboardComponent: z.string().optional(),
55
69
  }).optional(),
56
70
  });
57
-
58
- // <--- THIS WAS MISSING
59
71
  export type EntityDefinition = z.infer<typeof EntitySchema>;
60
72
 
61
73
 
62
74
  // ------------------------------------------------------------------
63
- // 3. App Specification (The Root Object)
75
+ // 3. UI LAYOUT DEFINITIONS (Presentation Layer)
64
76
  // ------------------------------------------------------------------
77
+
78
+ export const BlockSchema = z.object({
79
+ id: z.string(),
80
+ type: z.string(),
81
+ // ✅ FIX: Explicitly define Key and Value types
82
+ props: z.record(z.string(), z.any()),
83
+ layout: z.object({
84
+ colSpan: z.number().optional(),
85
+ rowSpan: z.number().optional(),
86
+ }).optional(),
87
+ });
88
+
89
+ export const PageSectionSchema = z.object({
90
+ id: z.string(),
91
+ title: z.string().optional(),
92
+ description: z.string().optional(),
93
+ layout: z.object({
94
+ columns: z.number().optional(),
95
+ variant: z.enum(['grid', 'stack', 'split']).optional()
96
+ }).optional(),
97
+ blocks: z.array(BlockSchema),
98
+ });
99
+
100
+ export const PageSchema = z.object({
101
+ id: z.string(),
102
+ slug: z.string(),
103
+ title: z.string(),
104
+ description: z.string().optional(),
105
+ icon: z.string().optional(),
106
+ sections: z.array(PageSectionSchema),
107
+ });
108
+ export type PageDefinition = z.infer<typeof PageSchema>;
109
+
110
+
111
+ // MASTER APP SPECIFICATION
65
112
  export const AppSpecificationSchema = z.object({
66
113
  meta: z.object({
67
114
  name: z.string(),
68
115
  version: z.string(),
69
116
  description: z.string().optional(),
70
117
  author: z.string().optional(),
71
- createdAt: z.string().optional(),
118
+ createdAt: z.string().optional(), // <--- ADD THIS LINE
119
+ }),
120
+
121
+ config: z.object({
122
+ theme: z.enum(['light', 'dark', 'system', 'corporate', 'midnight', 'blueprint']).default('system'),
123
+ mockMode: z.boolean().default(true),
124
+ brokerUrl: z.string().optional(),
72
125
  }),
126
+
127
+ modules: z.array(z.string()).optional(),
128
+ resources: z.array(ResourceSchema).optional(),
73
129
 
74
130
  domain: z.object({
75
131
  signals: z.array(SignalSchema),
76
132
  entities: z.array(EntitySchema),
77
133
  }),
78
134
 
79
- config: z.object({
80
- theme: z.enum(['light', 'dark', 'system', 'corporate', 'midnight', 'blueprint']).default('system'),
81
- mockMode: z.boolean().default(true),
82
- }),
135
+ pages: z.array(PageSchema).optional(),
83
136
  });
84
137
 
85
138
  export type AppSpecification = z.infer<typeof AppSpecificationSchema>;