@ramme-io/create-app 1.1.9 → 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.
- package/package.json +4 -3
- package/template/pkg.json +16 -13
- package/template/src/App.tsx +14 -7
- package/template/src/blocks/SmartTable.tsx +191 -0
- package/template/src/components/AutoForm.tsx +128 -0
- package/template/src/components/DynamicBlock.tsx +37 -31
- package/template/src/components/dev/GhostOverlay.tsx +26 -59
- package/template/src/config/app.manifest.ts +48 -48
- package/template/src/core/component-registry.tsx +21 -41
- package/template/src/core/data-seeder.ts +35 -0
- package/template/src/data/mockData.ts +163 -34
- package/template/src/generated/hooks.ts +34 -55
- package/template/src/hooks/useDataQuery.ts +84 -0
- package/template/src/hooks/useSignal.ts +43 -33
- package/template/src/hooks/useWorkflowEngine.ts +6 -0
- package/template/src/pages/Dashboard.tsx +43 -90
- package/template/src/pages/DynamicPage.tsx +54 -22
- package/template/src/templates/dashboard/DashboardLayout.tsx +2 -0
- package/template/src/templates/dashboard/dashboard.sitemap.ts +23 -73
- package/template/src/types/schema.ts +84 -31
|
@@ -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;
|
|
11
|
-
max?: number;
|
|
12
|
-
interval?: number; //
|
|
13
|
-
unit?: string;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
80
|
+
}, [topic, min, max, interval]);
|
|
71
81
|
|
|
72
82
|
return signal;
|
|
73
83
|
}
|
|
@@ -1,105 +1,58 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
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
|
-
|
|
17
|
-
const signals = useGeneratedSignals();
|
|
18
|
-
const { meta, domain } = appManifest;
|
|
8
|
+
const location = useLocation();
|
|
19
9
|
|
|
20
|
-
//
|
|
21
|
-
const
|
|
22
|
-
|
|
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={
|
|
28
|
-
description={
|
|
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
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
};
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { PageHeader, Alert } from '@ramme-io/ui';
|
|
2
|
+
import { PageHeader, Alert, Button, Icon } from '@ramme-io/ui';
|
|
3
3
|
import { appManifest } from '../config/app.manifest';
|
|
4
|
-
import { getComponent } from '../core/component-registry';
|
|
5
4
|
// Reuse the GhostOverlay for structure visualization
|
|
6
5
|
import { GhostOverlay } from '../components/dev/GhostOverlay';
|
|
7
|
-
// Import the
|
|
6
|
+
// Import the Worker Bee
|
|
8
7
|
import { DynamicBlock } from '../components/DynamicBlock';
|
|
8
|
+
// Import the Hook to control the overlay
|
|
9
|
+
import { useDevTools } from '../hooks/useDevTools';
|
|
9
10
|
|
|
10
11
|
interface DynamicPageProps {
|
|
11
12
|
pageId: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
const DynamicPage: React.FC<DynamicPageProps> = ({ pageId }) => {
|
|
15
|
-
// 1.
|
|
16
|
-
const
|
|
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);
|
|
17
21
|
|
|
18
22
|
if (!pageConfig) {
|
|
19
23
|
return (
|
|
20
24
|
<div className="p-8">
|
|
21
|
-
<Alert variant="danger" title="
|
|
25
|
+
<Alert variant="danger" title="Page Not Found">
|
|
22
26
|
The manifest does not contain a page with ID: <code>{pageId}</code>.
|
|
23
27
|
</Alert>
|
|
24
28
|
</div>
|
|
@@ -26,33 +30,61 @@ const DynamicPage: React.FC<DynamicPageProps> = ({ pageId }) => {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
return (
|
|
29
|
-
<div className="space-y-8 fade-in">
|
|
33
|
+
<div className="space-y-8 fade-in p-6">
|
|
30
34
|
<PageHeader
|
|
31
35
|
title={pageConfig.title}
|
|
32
|
-
description={pageConfig.description}
|
|
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
|
+
}
|
|
33
48
|
/>
|
|
34
49
|
|
|
35
|
-
{/* Render Sections */}
|
|
36
|
-
{pageConfig.sections.map((section:
|
|
50
|
+
{/* 4. Render Sections */}
|
|
51
|
+
{pageConfig.sections.map((section: any) => (
|
|
37
52
|
<div key={section.id} className="space-y-4">
|
|
38
53
|
{section.title && <h3 className="text-xl font-semibold">{section.title}</h3>}
|
|
39
54
|
|
|
40
55
|
<div
|
|
41
56
|
className="grid gap-6"
|
|
42
57
|
style={{
|
|
43
|
-
|
|
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))`
|
|
44
61
|
}}
|
|
45
62
|
>
|
|
46
|
-
{section.blocks.map(block =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
})}
|
|
56
88
|
</div>
|
|
57
89
|
</div>
|
|
58
90
|
))}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
+
}
|