@ramme-io/create-app 2.0.0 → 2.0.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/README.md +2 -2
- package/index.js +1 -1
- package/package.json +1 -1
- package/template/index.html +1 -1
- package/template/package.json +14 -5
- package/template/public/_redirects +1 -0
- package/template/src/App.tsx +3 -2
- package/template/src/components/AppHeader.tsx +5 -0
- package/template/src/components/ScrollToTop.tsx +6 -6
- package/template/src/features/ai/pages/AiChat.tsx +136 -37
- package/template/src/features/auth/AuthContext.tsx +16 -6
- package/template/src/features/config/AppConfigContext.tsx +2 -2
- package/template/src/features/docs/pages/EdgeTelemetryDemo.tsx +149 -0
- package/template/src/features/onboarding/pages/AboutRamme.tsx +12 -12
- package/template/src/features/onboarding/pages/PrototypeGallery.tsx +8 -6
- package/template/src/features/onboarding/pages/RammeFeatures.tsx +18 -17
- package/template/src/features/onboarding/pages/RammeTutorial.tsx +20 -11
- package/template/src/features/onboarding/pages/Welcome.tsx +6 -6
- package/template/src/features/styleguide/sections/tables/TablesSection.tsx +25 -5
- package/template/src/features/theme/pages/ThemeCustomizerPage.tsx +344 -256
- package/template/src/features/theme/utils/ThemeGenerator.logic.ts +587 -0
- package/template/src/hooks/__tests__/useStudioHotkeys.test.ts +100 -0
- package/template/src/hooks/useStudioHotkeys.ts +36 -0
- package/template/src/index.css +91 -1
- package/template/src/main.tsx +44 -2
- package/template/src/templates/dashboard/DashboardLayout.tsx +6 -1
- package/template/src/templates/dashboard/dashboard.sitemap.ts +1 -1
- package/template/src/templates/docs/docs.sitemap.ts +8 -0
- package/template/src/templates/settings/SettingsLayout.tsx +13 -26
- package/template/src/test/setup.ts +1 -0
- package/template/tsconfig.app.json +1 -0
- package/template/vite.config.ts +7 -0
package/README.md
CHANGED
|
@@ -15,9 +15,9 @@ This CLI scaffolds a production-ready React application with:
|
|
|
15
15
|
Get started in seconds. No global installation required.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm create @ramme-io/app
|
|
18
|
+
npm create @ramme-io/app my-app
|
|
19
19
|
# or
|
|
20
|
-
|
|
20
|
+
npm create @ramme-io/app my-app
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
You will be prompted to name your project.
|
package/index.js
CHANGED
|
@@ -19,7 +19,7 @@ const projectName = process.argv[2];
|
|
|
19
19
|
if (!projectName) {
|
|
20
20
|
console.error('❌ Error: Please specify the project directory.');
|
|
21
21
|
console.log('\n📖 Usage:');
|
|
22
|
-
console.log(' npm create @ramme-io/app
|
|
22
|
+
console.log(' npm create @ramme-io/app <project-name>\n');
|
|
23
23
|
process.exit(1);
|
|
24
24
|
}
|
|
25
25
|
|
package/package.json
CHANGED
package/template/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/orange.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>Ramme -
|
|
7
|
+
<title>Ramme | The High-Fidelity Creator Framework</title>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="root"></div>
|
package/template/package.json
CHANGED
|
@@ -7,14 +7,18 @@
|
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "tsc && vite build",
|
|
9
9
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
10
|
-
"preview": "vite preview"
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest"
|
|
11
13
|
},
|
|
12
14
|
"dependencies": {
|
|
13
15
|
"@ai-sdk/google": "^0.0.10",
|
|
14
16
|
"@google/generative-ai": "^0.24.1",
|
|
15
17
|
"@radix-ui/react-slot": "^1.2.4",
|
|
16
|
-
"@ramme-io/kernel": "
|
|
17
|
-
"@ramme-io/
|
|
18
|
+
"@ramme-io/kernel": "workspace:*",
|
|
19
|
+
"@ramme-io/shared": "workspace:*",
|
|
20
|
+
"@ramme-io/ui": "workspace:*",
|
|
21
|
+
"ably": "^2.21.0",
|
|
18
22
|
"ag-charts-community": "^13.0.0",
|
|
19
23
|
"ag-charts-react": "^13.0.0",
|
|
20
24
|
"ag-grid-community": "^31.3.1",
|
|
@@ -30,8 +34,8 @@
|
|
|
30
34
|
"react": "^18.3.1",
|
|
31
35
|
"react-datepicker": "^7.3.0",
|
|
32
36
|
"react-dom": "^18.3.1",
|
|
33
|
-
"react-router-dom": "6.22.3",
|
|
34
37
|
"react-markdown": "^10.1.0",
|
|
38
|
+
"react-router-dom": "6.22.3",
|
|
35
39
|
"react-select": "^5.8.0",
|
|
36
40
|
"react-syntax-highlighter": "^16.1.0",
|
|
37
41
|
"recharts": "^2.12.7",
|
|
@@ -41,14 +45,19 @@
|
|
|
41
45
|
"zustand": "^5.0.0"
|
|
42
46
|
},
|
|
43
47
|
"devDependencies": {
|
|
48
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
49
|
+
"@testing-library/react": "^16.3.2",
|
|
44
50
|
"@types/node": "^20.14.10",
|
|
45
51
|
"@types/react": "^18.3.3",
|
|
46
52
|
"@types/react-dom": "^18.3.0",
|
|
47
53
|
"@vitejs/plugin-react": "^4.3.1",
|
|
54
|
+
"@vitest/ui": "^2",
|
|
48
55
|
"autoprefixer": "^10.4.19",
|
|
56
|
+
"jsdom": "^29.0.0",
|
|
49
57
|
"postcss": "^8.4.39",
|
|
50
58
|
"tailwindcss": "^3.4.4",
|
|
51
59
|
"typescript": "^5.2.2",
|
|
52
|
-
"vite": "^5.3.1"
|
|
60
|
+
"vite": "^5.3.1",
|
|
61
|
+
"vitest": "^2.1.9"
|
|
53
62
|
}
|
|
54
63
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* /index.html 200
|
package/template/src/App.tsx
CHANGED
|
@@ -15,8 +15,6 @@ import DashboardLayout from './templates/dashboard/DashboardLayout';
|
|
|
15
15
|
import DocsLayout from './templates/docs/DocsLayout';
|
|
16
16
|
import SettingsLayout from './templates/settings/SettingsLayout';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
18
|
// --- SITEMAPS ---
|
|
21
19
|
import { dashboardSitemap } from './templates/dashboard/dashboard.sitemap';
|
|
22
20
|
import { docsSitemap } from './templates/docs/docs.sitemap';
|
|
@@ -24,6 +22,7 @@ import { settingsSitemap } from './templates/settings/settings.sitemap';
|
|
|
24
22
|
|
|
25
23
|
// --- UTILS ---
|
|
26
24
|
import ProtectedRoute from './components/ProtectedRoute';
|
|
25
|
+
import { useStudioHotkeys } from './hooks/useStudioHotkeys';
|
|
27
26
|
import NotFound from './components/NotFound';
|
|
28
27
|
import ScrollToTop from './components/ScrollToTop';
|
|
29
28
|
import HashLinkScroll from './components/HashLinkScroll';
|
|
@@ -82,6 +81,7 @@ const generateRoutes = (items: any[]) => {
|
|
|
82
81
|
};
|
|
83
82
|
|
|
84
83
|
function App() {
|
|
84
|
+
useStudioHotkeys();
|
|
85
85
|
const liveDashboardRoutes = useDynamicSitemap(dashboardSitemap);
|
|
86
86
|
|
|
87
87
|
return (
|
|
@@ -120,6 +120,7 @@ function App() {
|
|
|
120
120
|
|
|
121
121
|
{/* Settings */}
|
|
122
122
|
<Route path="/settings/*" element={<SettingsLayout />}>
|
|
123
|
+
<Route index element={<Navigate to="general" replace />} />
|
|
123
124
|
{generateRoutes(settingsSitemap)}
|
|
124
125
|
</Route>
|
|
125
126
|
|
|
@@ -5,12 +5,12 @@ const ScrollToTop = () => {
|
|
|
5
5
|
const { pathname } = useLocation();
|
|
6
6
|
|
|
7
7
|
useEffect(() => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
top: 0,
|
|
11
|
-
|
|
12
|
-
behavior: 'instant'
|
|
13
|
-
}
|
|
8
|
+
const container = document.getElementById('main-scroll-container');
|
|
9
|
+
if (container) {
|
|
10
|
+
container.scrollTo({ top: 0, left: 0, behavior: 'instant' as ScrollBehavior });
|
|
11
|
+
} else {
|
|
12
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'instant' as ScrollBehavior });
|
|
13
|
+
}
|
|
14
14
|
}, [pathname]);
|
|
15
15
|
|
|
16
16
|
return null;
|
|
@@ -1,61 +1,160 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Message,
|
|
8
|
-
PromptInput,
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
5
|
+
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
6
|
+
import {
|
|
7
|
+
Card, Conversation, Message, PromptInput, PageHeader, Icon,
|
|
9
8
|
} from '@ramme-io/ui';
|
|
10
|
-
|
|
9
|
+
|
|
10
|
+
const systemInstruction = `
|
|
11
|
+
You are the Ramme OS Assistant, part of the internal dev team.
|
|
12
|
+
Ramme is a high-performance, browser-native modular web-OS (Kernel + UI).
|
|
13
|
+
It is NOT the Instagram wrapper by Niklas Higle.
|
|
14
|
+
Answer as a Senior Engineer helping with a monorepo project.
|
|
15
|
+
Use numbered lists for steps and code blocks for commands.
|
|
16
|
+
`;
|
|
11
17
|
|
|
12
18
|
const AiChat: React.FC = () => {
|
|
13
|
-
const { messages, isLoading, sendMessage } = useMockChat();
|
|
14
19
|
const [input, setInput] = useState('');
|
|
20
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
21
|
+
const [apiHistory, setApiHistory] = useState<any[]>([]);
|
|
22
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const [messages, setMessages] = useState([
|
|
24
|
+
{ id: 'init', author: 'AI Agent', content: 'Ramme OS Assistant online. How can I help you build today?', isUser: false }
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
29
|
+
}, [messages, isLoading]);
|
|
30
|
+
|
|
31
|
+
const handleSubmit = async (e?: React.FormEvent) => {
|
|
32
|
+
if (e) e.preventDefault();
|
|
33
|
+
if (!input.trim() || isLoading) return;
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
sendMessage(input);
|
|
35
|
+
const userText = input;
|
|
36
|
+
setMessages(prev => [...prev, { id: `u-${Date.now()}`, author: 'You', content: userText, isUser: true }]);
|
|
19
37
|
setInput('');
|
|
38
|
+
setIsLoading(true);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const apiKey = localStorage.getItem('ramme_vault_gemini_key');
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
setMessages(prev => [...prev, {
|
|
44
|
+
id: `err-${Date.now()}`,
|
|
45
|
+
author: 'System',
|
|
46
|
+
content: 'No API key found. Please configure your Gemini API key in Settings → Theme Engine.',
|
|
47
|
+
isUser: false
|
|
48
|
+
}]);
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
contents: [
|
|
58
|
+
{ role: "user", parts: [{ text: `INSTRUCTION: ${systemInstruction}` }] },
|
|
59
|
+
{ role: "model", parts: [{ text: "Understood." }] },
|
|
60
|
+
...apiHistory,
|
|
61
|
+
{ role: "user", parts: [{ text: userText }] }
|
|
62
|
+
]
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
const aiText = data.candidates?.[0]?.content?.parts?.[0]?.text || "No response.";
|
|
68
|
+
|
|
69
|
+
setMessages(prev => [...prev, { id: `ai-${Date.now()}`, author: 'AI Agent', content: aiText, isUser: false }]);
|
|
70
|
+
setApiHistory([...apiHistory, { role: "user", parts: [{ text: userText }] }, { role: "model", parts: [{ text: aiText }] }]);
|
|
71
|
+
} catch (err: any) {
|
|
72
|
+
setMessages(prev => [...prev, { id: `err-${Date.now()}`, author: 'System', content: `Error: ${err.message}`, isUser: false }]);
|
|
73
|
+
} finally {
|
|
74
|
+
setIsLoading(false);
|
|
75
|
+
}
|
|
20
76
|
};
|
|
21
77
|
|
|
22
78
|
return (
|
|
23
|
-
<div className="p-4 sm:p-6 lg:p-8 space-y-
|
|
79
|
+
<div className="p-4 sm:p-6 lg:p-8 space-y-6 h-[calc(100vh-64px)] flex flex-col">
|
|
24
80
|
<PageHeader
|
|
25
81
|
title="AI Assistant"
|
|
26
82
|
description="Full-screen command center for Ramme AI."
|
|
27
83
|
/>
|
|
28
84
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
85
|
+
<Card className="flex-1 flex flex-col min-h-0 shadow-sm border-border/50 overflow-hidden">
|
|
86
|
+
<div className="px-4 py-2 border-b flex justify-between items-center bg-muted/20">
|
|
87
|
+
<div className="flex items-center gap-2">
|
|
88
|
+
<div className={`w-2 h-2 rounded-full ${isLoading ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500'}`} />
|
|
89
|
+
<span className="font-bold text-[10px] uppercase tracking-widest opacity-60">Ramme Assistant</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4 custom-scrollbar">
|
|
34
94
|
<Conversation>
|
|
35
95
|
{messages.map((msg) => (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
96
|
+
<div key={msg.id} className={`flex ${msg.isUser ? 'justify-end' : 'justify-start'} mb-3`}>
|
|
97
|
+
<div className="max-w-[98%] w-full">
|
|
98
|
+
<Message
|
|
99
|
+
author={msg.author}
|
|
100
|
+
isUser={msg.isUser}
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
content={
|
|
103
|
+
<div className="prose prose-sm dark:prose-invert max-w-none text-foreground leading-relaxed">
|
|
104
|
+
<ReactMarkdown
|
|
105
|
+
remarkPlugins={[remarkGfm]}
|
|
106
|
+
components={{
|
|
107
|
+
p: ({children}) => <p className="mb-4 last:mb-0">{children}</p>,
|
|
108
|
+
ol: ({children}) => <ol className="list-decimal pl-8 mb-4 space-y-2">{children}</ol>,
|
|
109
|
+
ul: ({children}) => <ul className="list-disc pl-8 mb-4 space-y-2">{children}</ul>,
|
|
110
|
+
li: ({children}) => <li className="pl-1">{children}</li>,
|
|
111
|
+
code({ node, inline, className, children, ...props }: any) {
|
|
112
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
113
|
+
return !inline && match ? (
|
|
114
|
+
<div className="my-5 rounded-lg overflow-hidden border border-border bg-[#0d0d0d] shadow-xl">
|
|
115
|
+
<div className="px-3 py-1 bg-white/5 border-b border-white/10 flex justify-between">
|
|
116
|
+
<span className="text-[9px] text-muted-foreground uppercase">{match[1]}</span>
|
|
117
|
+
</div>
|
|
118
|
+
<SyntaxHighlighter
|
|
119
|
+
style={oneDark}
|
|
120
|
+
language={match[1]}
|
|
121
|
+
PreTag="div"
|
|
122
|
+
customStyle={{ margin: 0, padding: '1.2rem', fontSize: '0.8rem', background: 'transparent' }}
|
|
123
|
+
{...props}
|
|
124
|
+
>
|
|
125
|
+
{String(children).replace(/\n$/, '')}
|
|
126
|
+
</SyntaxHighlighter>
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<code className="bg-muted px-1.5 py-0.5 rounded font-mono text-primary text-[0.9em]" {...props}>
|
|
130
|
+
{children}
|
|
131
|
+
</code>
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{msg.content}
|
|
137
|
+
</ReactMarkdown>
|
|
138
|
+
</div>
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
45
143
|
))}
|
|
46
|
-
{isLoading && <Message author="AI Agent" isUser={false} loading />}
|
|
47
144
|
</Conversation>
|
|
48
145
|
</div>
|
|
49
|
-
|
|
50
|
-
<div className="p-4 border-t bg-card">
|
|
51
|
-
<PromptInput
|
|
52
|
-
value={input}
|
|
53
|
-
onChange={(e) => setInput(e.target.value)}
|
|
54
|
-
onSubmit={handleSubmit}
|
|
55
|
-
placeholder="Type a command or ask a question..."
|
|
56
|
-
/>
|
|
146
|
+
|
|
147
|
+
<div className="p-4 border-t bg-card/40 backdrop-blur-sm">
|
|
148
|
+
<PromptInput value={input} onChange={(e) => setInput(e.target.value)} onSubmit={handleSubmit} placeholder="Ask about Ramme..." />
|
|
57
149
|
</div>
|
|
58
150
|
</Card>
|
|
151
|
+
|
|
152
|
+
<style dangerouslySetInnerHTML={{ __html: `
|
|
153
|
+
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
|
154
|
+
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
|
155
|
+
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgb(var(--app-primary-color), 0.2); border-radius: 10px; }
|
|
156
|
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgb(var(--app-primary-color), 0.5); }
|
|
157
|
+
`}} />
|
|
59
158
|
</div>
|
|
60
159
|
);
|
|
61
160
|
};
|
|
@@ -7,6 +7,19 @@ const EMERGENCY_ADMIN: User = {
|
|
|
7
7
|
id: 'usr_admin_force',
|
|
8
8
|
name: 'Alex Admin (Recovery)',
|
|
9
9
|
email: 'alex@example.com',
|
|
10
|
+
password: 'admin',
|
|
11
|
+
role: 'admin',
|
|
12
|
+
status: 'active',
|
|
13
|
+
joinedAt: new Date().toISOString(),
|
|
14
|
+
lastActive: new Date().toISOString()
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// 🌐 DEFAULT GUEST: Auto-login for frictionless marketing site experience
|
|
18
|
+
const DEFAULT_GUEST: User = {
|
|
19
|
+
id: 'usr_guest',
|
|
20
|
+
name: 'Guest Creator',
|
|
21
|
+
email: 'guest@ramme.io',
|
|
22
|
+
password: 'guest',
|
|
10
23
|
role: 'admin',
|
|
11
24
|
status: 'active',
|
|
12
25
|
joinedAt: new Date().toISOString(),
|
|
@@ -27,10 +40,10 @@ interface AuthContextType {
|
|
|
27
40
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
28
41
|
|
|
29
42
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
30
|
-
const [user, setUser] = useState<User | null>(
|
|
31
|
-
const [isLoading, setIsLoading] = useState(
|
|
43
|
+
const [user, setUser] = useState<User | null>(DEFAULT_GUEST);
|
|
44
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
32
45
|
|
|
33
|
-
// 1. Mount: Check for existing session
|
|
46
|
+
// 1. Mount: Check for existing session, fall back to guest
|
|
34
47
|
useEffect(() => {
|
|
35
48
|
const storedUser = localStorage.getItem(SESSION_KEY);
|
|
36
49
|
if (storedUser) {
|
|
@@ -38,15 +51,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|
|
38
51
|
const parsed = JSON.parse(storedUser);
|
|
39
52
|
if (parsed && parsed.id) {
|
|
40
53
|
setUser(parsed);
|
|
41
|
-
} else {
|
|
42
|
-
localStorage.removeItem(SESSION_KEY);
|
|
43
54
|
}
|
|
44
55
|
} catch (e) {
|
|
45
56
|
console.error("Failed to parse session", e);
|
|
46
57
|
localStorage.removeItem(SESSION_KEY);
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
|
-
setIsLoading(false);
|
|
50
60
|
}, []);
|
|
51
61
|
|
|
52
62
|
// 2. Action: Login
|
|
@@ -14,8 +14,8 @@ export const AppConfigProvider = ({ children }: { children: ReactNode }) => {
|
|
|
14
14
|
|
|
15
15
|
const [enableCRT, setEnableCRT] = useState(() => {
|
|
16
16
|
const stored = localStorage.getItem('app_enable_crt');
|
|
17
|
-
// Default to
|
|
18
|
-
return stored !== null ? JSON.parse(stored) :
|
|
17
|
+
// Default to false for clean marketing site clarity
|
|
18
|
+
return stored !== null ? JSON.parse(stored) : false;
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useDevice } from '@ramme-io/kernel';
|
|
3
|
+
import { CircularDial, WaterPumpControl, TelemetryLog } from '@ramme-io/ui';
|
|
4
|
+
import type { TelemetryLogRow } from '@ramme-io/ui';
|
|
5
|
+
import type { PumpMode } from '@ramme-io/ui';
|
|
6
|
+
import { StandardPageLayout } from '../../../components/layout/StandardPageLayout';
|
|
7
|
+
|
|
8
|
+
const EdgeTelemetryDemo: React.FC = () => {
|
|
9
|
+
const telemetry = useDevice();
|
|
10
|
+
const [fps, setFps] = useState(0);
|
|
11
|
+
const [pumpMode, setPumpMode] = useState<PumpMode>('AUTO');
|
|
12
|
+
const [targetMoisture, setTargetMoisture] = useState(40);
|
|
13
|
+
const [logData, setLogData] = useState<TelemetryLogRow[]>([]);
|
|
14
|
+
const logBuffer = useRef<TelemetryLogRow[]>([]);
|
|
15
|
+
const frameCount = useRef(0);
|
|
16
|
+
const rafId = useRef(0);
|
|
17
|
+
|
|
18
|
+
// Append incoming telemetry to the historical log buffer (capped at 100)
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!telemetry) return;
|
|
21
|
+
const row: TelemetryLogRow = {
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
soilMoisture: telemetry.soilMoisture,
|
|
24
|
+
ambientLight: telemetry.ambientLight,
|
|
25
|
+
temperature: telemetry.temperature,
|
|
26
|
+
};
|
|
27
|
+
logBuffer.current = [...logBuffer.current.slice(-99), row];
|
|
28
|
+
setLogData(logBuffer.current);
|
|
29
|
+
}, [telemetry]);
|
|
30
|
+
|
|
31
|
+
// Continuous RAF loop tallies frames; 1s interval reads & resets the tally
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const tick = () => {
|
|
34
|
+
frameCount.current += 1;
|
|
35
|
+
rafId.current = requestAnimationFrame(tick);
|
|
36
|
+
};
|
|
37
|
+
rafId.current = requestAnimationFrame(tick);
|
|
38
|
+
|
|
39
|
+
const interval = setInterval(() => {
|
|
40
|
+
setFps(frameCount.current);
|
|
41
|
+
frameCount.current = 0;
|
|
42
|
+
}, 1000);
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
cancelAnimationFrame(rafId.current);
|
|
46
|
+
clearInterval(interval);
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<StandardPageLayout
|
|
52
|
+
title="Edge Telemetry Engine"
|
|
53
|
+
description="Hardware-in-the-loop simulation testing 50Hz MQTT ingestion and Zero Jank UI rendering."
|
|
54
|
+
>
|
|
55
|
+
{/* Status Bar */}
|
|
56
|
+
<div className="flex items-center gap-4 p-4 rounded-2xl bg-card border border-border font-mono text-xs mb-8">
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
|
59
|
+
<span className="font-bold text-foreground uppercase tracking-widest">Kernel Link Active</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="ml-auto flex items-center gap-4">
|
|
62
|
+
<span className={fps > 20 ? "text-red-500 font-bold" : "text-green-500 font-bold"}>
|
|
63
|
+
{fps} FPS
|
|
64
|
+
</span>
|
|
65
|
+
<span className="text-muted-foreground">
|
|
66
|
+
Target: ~15 FPS | Simulator: 50Hz
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Telemetry Grid */}
|
|
72
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
73
|
+
{/* Soil Moisture Dial */}
|
|
74
|
+
<div className="flex flex-col items-center gap-4 p-8 rounded-3xl bg-card border border-border">
|
|
75
|
+
{telemetry ? (
|
|
76
|
+
<CircularDial
|
|
77
|
+
value={telemetry.soilMoisture ?? 0}
|
|
78
|
+
label="Soil Moisture"
|
|
79
|
+
unit="%"
|
|
80
|
+
size={180}
|
|
81
|
+
/>
|
|
82
|
+
) : (
|
|
83
|
+
<div className="text-muted-foreground text-sm">Awaiting telemetry...</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Ambient Light Dial */}
|
|
88
|
+
<div className="flex flex-col items-center gap-4 p-8 rounded-3xl bg-card border border-border">
|
|
89
|
+
{telemetry ? (
|
|
90
|
+
<CircularDial
|
|
91
|
+
value={telemetry.ambientLight ?? 0}
|
|
92
|
+
min={0}
|
|
93
|
+
max={100000}
|
|
94
|
+
label="Ambient Light"
|
|
95
|
+
unit=" lx"
|
|
96
|
+
size={180}
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="text-muted-foreground text-sm">Awaiting telemetry...</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Temperature Dial */}
|
|
104
|
+
<div className="flex flex-col items-center gap-4 p-8 rounded-3xl bg-card border border-border">
|
|
105
|
+
{telemetry ? (
|
|
106
|
+
<CircularDial
|
|
107
|
+
value={telemetry.temperature ?? 0}
|
|
108
|
+
min={-40}
|
|
109
|
+
max={125}
|
|
110
|
+
label="Temperature"
|
|
111
|
+
unit="°C"
|
|
112
|
+
size={180}
|
|
113
|
+
/>
|
|
114
|
+
) : (
|
|
115
|
+
<div className="text-muted-foreground text-sm">Awaiting telemetry...</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Command: Water Pump Actuation */}
|
|
121
|
+
<div className="mt-8">
|
|
122
|
+
<WaterPumpControl
|
|
123
|
+
pumpMode={pumpMode}
|
|
124
|
+
targetMoisture={targetMoisture}
|
|
125
|
+
onCommand={(payload) => {
|
|
126
|
+
setPumpMode(payload.pumpMode);
|
|
127
|
+
setTargetMoisture(payload.targetMoisture);
|
|
128
|
+
console.log('[MQTT PUBLISH MOCK]', payload);
|
|
129
|
+
}}
|
|
130
|
+
className="max-w-md"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Raw Payload Debug */}
|
|
135
|
+
<div className="mt-8 p-6 rounded-2xl bg-card border border-border">
|
|
136
|
+
<div className="font-mono text-[10px] uppercase tracking-[0.3em] text-muted-foreground mb-3 font-bold">Raw Smoothed Payload</div>
|
|
137
|
+
<pre className="font-mono text-sm text-foreground/80 leading-relaxed tabular-nums">
|
|
138
|
+
{JSON.stringify(telemetry, null, 2)}
|
|
139
|
+
</pre>
|
|
140
|
+
</div>
|
|
141
|
+
{/* Telemetry Log — AG Grid virtualized scroll */}
|
|
142
|
+
<div className="mt-8">
|
|
143
|
+
<TelemetryLog rowData={logData} maxRows={100} height="320px" />
|
|
144
|
+
</div>
|
|
145
|
+
</StandardPageLayout>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default EdgeTelemetryDemo;
|
|
@@ -18,7 +18,7 @@ const AboutRamme: React.FC = () => {
|
|
|
18
18
|
|
|
19
19
|
{/* --- 01: THE MANIFESTO --- */}
|
|
20
20
|
<header className="space-y-8 pt-20 border-l-4 pl-12" style={{ borderColor: 'rgb(var(--app-primary-color))' }}>
|
|
21
|
-
<Badge variant="primary" className="uppercase tracking-[0.3em] text-[10px] px-4 py-1.5 border-primary/
|
|
21
|
+
<Badge variant="primary" className="uppercase tracking-[0.3em] text-[10px] px-4 py-1.5 border-primary/40 bg-primary/10 text-primary">
|
|
22
22
|
Core Philosophy
|
|
23
23
|
</Badge>
|
|
24
24
|
<h1 className="text-7xl md:text-[110px] font-black tracking-[-0.02em] leading-[0.8] text-foreground uppercase">
|
|
@@ -40,19 +40,19 @@ const AboutRamme: React.FC = () => {
|
|
|
40
40
|
<p className="text-muted-foreground leading-relaxed text-lg">
|
|
41
41
|
Traditional prototypes are fragile. They break when a user clicks outside a hotspot or refreshes the page. Ramme uses a <span style={{ color: 'rgb(var(--app-info-color))' }} className="font-bold underline decoration-info/20">Local-First Kernel</span> that simulates a real database, real authentication, and real logic.
|
|
42
42
|
</p>
|
|
43
|
-
<div className="p-8 rounded-3xl bg-muted/5 border border-
|
|
43
|
+
<div className="p-8 rounded-3xl bg-muted/5 border border-border italic text-sm text-foreground/60 leading-relaxed shadow-inner">
|
|
44
44
|
"We didn't just want a UI kit. We wanted an OS for ideas—a environment where the data lives and breathes alongside the design."
|
|
45
45
|
</div>
|
|
46
46
|
</div>
|
|
47
47
|
<div className="grid grid-cols-2 gap-6">
|
|
48
|
-
<Card className="p-8 bg-
|
|
48
|
+
<Card className="p-8 bg-card/60 border-border flex flex-col gap-5 rounded-[2rem] hover:translate-y-[-4px] transition-transform">
|
|
49
49
|
<Icon name="database" style={{ color: 'rgb(var(--app-success-color))' }} size={28} />
|
|
50
50
|
<div className="space-y-2">
|
|
51
51
|
<div className="text-[10px] font-black uppercase tracking-widest text-success/60">Local Engine</div>
|
|
52
52
|
<p className="text-xs text-muted-foreground font-medium leading-relaxed">Auto-persisted JSON state that lives in the browser's kernel.</p>
|
|
53
53
|
</div>
|
|
54
54
|
</Card>
|
|
55
|
-
<Card className="p-8 bg-
|
|
55
|
+
<Card className="p-8 bg-card/60 border-border flex flex-col gap-5 rounded-[2rem] hover:translate-y-[-4px] transition-transform">
|
|
56
56
|
<Icon name="shield-check" style={{ color: 'rgb(var(--app-primary-color))' }} size={28} />
|
|
57
57
|
<div className="space-y-2">
|
|
58
58
|
<div className="text-[10px] font-black uppercase tracking-widest text-primary/60">Auth Simulation</div>
|
|
@@ -68,12 +68,12 @@ const AboutRamme: React.FC = () => {
|
|
|
68
68
|
<div className="h-2 w-2 rounded-full shadow-[0_0_10px_rgba(var(--app-info-color),0.5)]" style={{ backgroundColor: 'rgb(var(--app-info-color))' }} />
|
|
69
69
|
<h3 className="text-[10px] font-black uppercase tracking-widest opacity-40 text-foreground">Entry Point</h3>
|
|
70
70
|
</div>
|
|
71
|
-
<Card className="p-0 overflow-hidden rounded-3xl border-
|
|
71
|
+
<Card className="p-0 overflow-hidden rounded-3xl border-border shadow-2xl">
|
|
72
72
|
<RammeCodeBlock
|
|
73
73
|
variant="info"
|
|
74
74
|
title="Interactive Terminal"
|
|
75
75
|
language="bash"
|
|
76
|
-
code="
|
|
76
|
+
code="npm create @ramme-io/app my-prototype"
|
|
77
77
|
/>
|
|
78
78
|
</Card>
|
|
79
79
|
</section>
|
|
@@ -127,7 +127,7 @@ set({ lastLogin: new Date() });`}
|
|
|
127
127
|
{ label: 'Tailwind CSS', icon: 'wind', color: 'text-sky-400' }
|
|
128
128
|
].map((tech) => (
|
|
129
129
|
<div key={tech.label} className="flex flex-col items-center gap-4 group cursor-default">
|
|
130
|
-
<div className="p-5 rounded-2xl bg-
|
|
130
|
+
<div className="p-5 rounded-2xl bg-muted/10 border border-border group-hover:border-border/70 transition-all">
|
|
131
131
|
<Icon name={tech.icon as any} className={tech.color} size={32} />
|
|
132
132
|
</div>
|
|
133
133
|
<span className="font-bold tracking-tight text-sm opacity-50 group-hover:opacity-100 transition-opacity">{tech.label}</span>
|
|
@@ -137,18 +137,18 @@ set({ lastLogin: new Date() });`}
|
|
|
137
137
|
</section>
|
|
138
138
|
|
|
139
139
|
{/* --- FINAL CTA --- */}
|
|
140
|
-
<footer className="text-center space-y-12 bg-
|
|
140
|
+
<footer className="text-center space-y-12 bg-card/60 border border-border p-20 rounded-[4rem] shadow-2xl relative overflow-hidden group">
|
|
141
141
|
<div className="relative z-10 space-y-8">
|
|
142
142
|
<h2 className="text-6xl font-black tracking-tight text-foreground uppercase leading-[0.9]">
|
|
143
143
|
Ready to bypass the <br/>
|
|
144
144
|
<span style={{ color: 'rgb(var(--app-primary-color))' }}>boilerplate?</span>
|
|
145
145
|
</h2>
|
|
146
146
|
<div className="flex flex-col sm:flex-row justify-center gap-6 pt-4">
|
|
147
|
-
<Button size="lg" className="px-12 h-20 text-xl font-black rounded-full shadow-2xl hover:scale-105 transition-transform" onClick={() => navigate('/
|
|
148
|
-
|
|
147
|
+
<Button size="lg" className="px-12 h-20 text-xl font-black rounded-full shadow-2xl hover:scale-105 transition-transform" onClick={() => navigate('/docs/styleguide/templates')}>
|
|
148
|
+
Browse the Style Guide
|
|
149
149
|
</Button>
|
|
150
|
-
<Button size="lg" variant="outline" className="px-12 h-20 text-xl font-black rounded-full border-
|
|
151
|
-
|
|
150
|
+
<Button size="lg" variant="outline" className="px-12 h-20 text-xl font-black rounded-full border-border hover:bg-muted/10" onClick={() => navigate('/dashboard/tutorial')}>
|
|
151
|
+
Start Tutorial
|
|
152
152
|
</Button>
|
|
153
153
|
</div>
|
|
154
154
|
</div>
|