@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { useAppConfig } from '../features/config/AppConfigContext';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Global keyboard shortcuts for the Ramme Studio.
|
|
6
|
+
* Ctrl+Shift+C — Toggle CRT/LCD scanline effect with subtle flash feedback.
|
|
7
|
+
*/
|
|
8
|
+
export function useStudioHotkeys() {
|
|
9
|
+
const { enableCRT, setEnableCRT } = useAppConfig();
|
|
10
|
+
const enableCRTRef = useRef(enableCRT);
|
|
11
|
+
enableCRTRef.current = enableCRT;
|
|
12
|
+
|
|
13
|
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
14
|
+
// Only respond to Ctrl+Shift+C
|
|
15
|
+
if (!e.ctrlKey || !e.shiftKey || e.key !== 'C') return;
|
|
16
|
+
|
|
17
|
+
// Don't fire when user is typing in form fields
|
|
18
|
+
const tag = (e.target as HTMLElement)?.tagName;
|
|
19
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
20
|
+
if ((e.target as HTMLElement)?.isContentEditable) return;
|
|
21
|
+
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setEnableCRT(!enableCRTRef.current);
|
|
24
|
+
|
|
25
|
+
// Subtle flash feedback
|
|
26
|
+
document.body.classList.remove('studio-flash');
|
|
27
|
+
void document.body.offsetWidth; // force reflow to restart animation
|
|
28
|
+
document.body.classList.add('studio-flash');
|
|
29
|
+
setTimeout(() => document.body.classList.remove('studio-flash'), 250);
|
|
30
|
+
}, [setEnableCRT]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
34
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
35
|
+
}, [handleKeyDown]);
|
|
36
|
+
}
|
package/template/src/index.css
CHANGED
|
@@ -3,11 +3,92 @@
|
|
|
3
3
|
@tailwind utilities;
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
/* Smooth theme morphing — transitions all CSS variable-driven colors */
|
|
7
|
+
html {
|
|
8
|
+
transition: background-color 0.5s ease, color 0.5s ease;
|
|
9
|
+
}
|
|
10
|
+
html *,
|
|
11
|
+
html *::before,
|
|
12
|
+
html *::after {
|
|
13
|
+
transition: background-color 0.5s ease, color 0.3s ease, border-color 0.4s ease, box-shadow 0.4s ease;
|
|
14
|
+
}
|
|
15
|
+
/* AG Grid needs instant hover/selection feedback — exempt from theme morphing */
|
|
16
|
+
.ag-theme-quartz *,
|
|
17
|
+
.ag-theme-quartz *::before,
|
|
18
|
+
.ag-theme-quartz *::after {
|
|
19
|
+
transition: none !important;
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
/* Add this new rule */
|
|
7
23
|
.sidebar-panel {
|
|
8
24
|
min-width: 220px !important;
|
|
9
25
|
}
|
|
10
26
|
|
|
27
|
+
/* ===================================================================
|
|
28
|
+
AG GRID THEME BRIDGE FOR RAMME
|
|
29
|
+
Uses --app-* CSS variables which are set by:
|
|
30
|
+
- Standard themes: CSS classes (.dark, .midnight, etc.) in UI index.css
|
|
31
|
+
- AI/Custom themes: inline styles on <html> via ThemeContext
|
|
32
|
+
=================================================================== */
|
|
33
|
+
|
|
34
|
+
/* --- AG Grid element overrides --- */
|
|
35
|
+
/* Uses ONLY base --app-* vars that the AI theme generator sets as inline styles on <html> */
|
|
36
|
+
.ag-theme-quartz.ag-theme-ramme .ag-root-wrapper {
|
|
37
|
+
background-color: rgb(var(--app-card-bg-color)) !important;
|
|
38
|
+
color: rgb(var(--app-text-color)) !important;
|
|
39
|
+
border-color: rgb(var(--app-border-color)) !important;
|
|
40
|
+
}
|
|
41
|
+
.ag-theme-quartz.ag-theme-ramme .ag-header,
|
|
42
|
+
.ag-theme-quartz.ag-theme-ramme .ag-header-row {
|
|
43
|
+
background-color: rgb(var(--app-muted-bg)) !important;
|
|
44
|
+
color: rgb(var(--app-text-color)) !important;
|
|
45
|
+
}
|
|
46
|
+
.ag-theme-quartz.ag-theme-ramme .ag-header-cell {
|
|
47
|
+
background-color: transparent !important;
|
|
48
|
+
}
|
|
49
|
+
.ag-theme-quartz.ag-theme-ramme .ag-header-cell-label {
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
}
|
|
52
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row,
|
|
53
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row-odd,
|
|
54
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row-even,
|
|
55
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row-no-focus {
|
|
56
|
+
background-color: rgb(var(--app-card-bg-color)) !important;
|
|
57
|
+
color: rgb(var(--app-text-color)) !important;
|
|
58
|
+
border-bottom-color: rgb(var(--app-border-color)) !important;
|
|
59
|
+
}
|
|
60
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row:hover,
|
|
61
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row-hover,
|
|
62
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row.ag-row-hover {
|
|
63
|
+
background-color: rgb(var(--app-bg-color)) !important;
|
|
64
|
+
}
|
|
65
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row-hover .ag-cell,
|
|
66
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row:hover .ag-cell {
|
|
67
|
+
background-color: rgb(var(--app-bg-color)) !important;
|
|
68
|
+
}
|
|
69
|
+
.ag-theme-quartz.ag-theme-ramme .ag-row-selected {
|
|
70
|
+
background-color: rgba(var(--app-primary-color), 0.15) !important;
|
|
71
|
+
}
|
|
72
|
+
.ag-theme-quartz.ag-theme-ramme .ag-body-viewport {
|
|
73
|
+
background-color: rgb(var(--app-card-bg-color)) !important;
|
|
74
|
+
}
|
|
75
|
+
.ag-theme-quartz.ag-theme-ramme .ag-floating-filter {
|
|
76
|
+
background-color: rgb(var(--app-card-bg-color)) !important;
|
|
77
|
+
}
|
|
78
|
+
.ag-theme-quartz.ag-theme-ramme .ag-floating-filter-input {
|
|
79
|
+
background-color: rgb(var(--app-input-bg-color)) !important;
|
|
80
|
+
color: rgb(var(--app-text-color)) !important;
|
|
81
|
+
}
|
|
82
|
+
.ag-theme-quartz.ag-theme-ramme .ag-paging-panel {
|
|
83
|
+
background-color: rgb(var(--app-card-bg-color)) !important;
|
|
84
|
+
color: rgb(var(--app-muted-text)) !important;
|
|
85
|
+
border-top: 1px solid rgb(var(--app-border-color)) !important;
|
|
86
|
+
}
|
|
87
|
+
.ag-theme-quartz.ag-theme-ramme .ag-cell {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
11
92
|
|
|
12
93
|
/*
|
|
13
94
|
=================================================================
|
|
@@ -68,4 +149,13 @@ body.crt-mode::after {
|
|
|
68
149
|
@keyframes crtAnimation {
|
|
69
150
|
0% { background-position: 0 0; }
|
|
70
151
|
100% { background-position: 0 10000%; }
|
|
71
|
-
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Studio Hotkey flash feedback (Ctrl+Shift+C) */
|
|
155
|
+
@keyframes studio-flash {
|
|
156
|
+
0% { box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.6); }
|
|
157
|
+
100% { box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0); }
|
|
158
|
+
}
|
|
159
|
+
.studio-flash {
|
|
160
|
+
animation: studio-flash 200ms ease-out;
|
|
161
|
+
}
|
package/template/src/main.tsx
CHANGED
|
@@ -12,6 +12,8 @@ import App from './App.tsx';
|
|
|
12
12
|
|
|
13
13
|
import { applyThemeToDom, loadGoogleFont } from './features/theme/utils/themeUtils.ts'; // adjust path as needed
|
|
14
14
|
|
|
15
|
+
import 'ag-grid-community/styles/ag-grid.css';
|
|
16
|
+
import 'ag-grid-community/styles/ag-theme-quartz.css';
|
|
15
17
|
import "@ramme-io/ui/index.css";
|
|
16
18
|
import './index.css';
|
|
17
19
|
import 'ag-grid-enterprise';
|
|
@@ -23,7 +25,7 @@ const CURRENT_SCHEMA_VERSION = 'v2-added-passwords';
|
|
|
23
25
|
|
|
24
26
|
// --- THEME HYDRATION LOGIC ---
|
|
25
27
|
const ThemeHydrator = () => {
|
|
26
|
-
React.
|
|
28
|
+
const applyPersistedTheme = React.useCallback(() => {
|
|
27
29
|
const savedTheme = localStorage.getItem('ramme_persisted_theme');
|
|
28
30
|
if (savedTheme) {
|
|
29
31
|
try {
|
|
@@ -40,7 +42,47 @@ const ThemeHydrator = () => {
|
|
|
40
42
|
}
|
|
41
43
|
}, []);
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
// Apply immediately on mount
|
|
47
|
+
applyPersistedTheme();
|
|
48
|
+
|
|
49
|
+
// Re-apply on any navigation or visibility change
|
|
50
|
+
const handleVisibilityChange = () => {
|
|
51
|
+
if (document.visibilityState === 'visible') {
|
|
52
|
+
applyPersistedTheme();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Watch for DOM mutations that might reset the theme
|
|
57
|
+
const observer = new MutationObserver(() => {
|
|
58
|
+
const savedTheme = localStorage.getItem('ramme_persisted_theme');
|
|
59
|
+
if (savedTheme) {
|
|
60
|
+
const theme = JSON.parse(savedTheme);
|
|
61
|
+
// Check if theme has been reset by comparing a key variable
|
|
62
|
+
const currentPrimary = getComputedStyle(document.documentElement)
|
|
63
|
+
.getPropertyValue('--app-primary-color').trim();
|
|
64
|
+
const expectedPrimary = theme.cssVars['--app-primary-color'];
|
|
65
|
+
|
|
66
|
+
if (currentPrimary && expectedPrimary && currentPrimary !== expectedPrimary) {
|
|
67
|
+
applyPersistedTheme();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
observer.observe(document.documentElement, {
|
|
73
|
+
attributes: true,
|
|
74
|
+
attributeFilter: ['class', 'style']
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
observer.disconnect();
|
|
81
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
82
|
+
};
|
|
83
|
+
}, [applyPersistedTheme]);
|
|
84
|
+
|
|
85
|
+
return null;
|
|
44
86
|
};
|
|
45
87
|
|
|
46
88
|
const runMigration = () => {
|
|
@@ -83,6 +83,11 @@ const DashboardLayout: React.FC = () => {
|
|
|
83
83
|
label="Documentation"
|
|
84
84
|
onClick={() => navigate('/docs')}
|
|
85
85
|
/>
|
|
86
|
+
<ButtonItem
|
|
87
|
+
icon="cpu"
|
|
88
|
+
label="Bodewell"
|
|
89
|
+
onClick={() => navigate('/bodewell/fleet')}
|
|
90
|
+
/>
|
|
86
91
|
<ButtonItem
|
|
87
92
|
icon="refresh-cw"
|
|
88
93
|
label="Clear Cache & Reset"
|
|
@@ -94,7 +99,7 @@ const DashboardLayout: React.FC = () => {
|
|
|
94
99
|
|
|
95
100
|
|
|
96
101
|
|
|
97
|
-
<main className="flex-1 overflow-y-auto bg-muted/20 relative w-full">
|
|
102
|
+
<main id="main-scroll-container" className="flex-1 overflow-y-auto bg-muted/20 relative w-full">
|
|
98
103
|
<Outlet />
|
|
99
104
|
</main>
|
|
100
105
|
|
|
@@ -10,7 +10,7 @@ import RammeTutorial from '../../features/onboarding/pages/RammeTutorial';
|
|
|
10
10
|
import AiChat from '../../features/ai/pages/AiChat';
|
|
11
11
|
|
|
12
12
|
// ✅ NEW: Import from the Feature Domain (Clean API)
|
|
13
|
-
import { UsersPage } from '../../features/users';
|
|
13
|
+
import { UsersPage } from '../../features/users';
|
|
14
14
|
|
|
15
15
|
export const dashboardSitemap: SitemapEntry[] = [
|
|
16
16
|
// 1. The New Landing Page
|
|
@@ -6,6 +6,7 @@ import { OverviewPage } from '../../features/overview';
|
|
|
6
6
|
import AiChat from '../../features/ai/pages/AiChat';
|
|
7
7
|
|
|
8
8
|
import Styleguide from '../../features/styleguide/Styleguide';
|
|
9
|
+
import EdgeTelemetryDemo from '../../features/docs/pages/EdgeTelemetryDemo';
|
|
9
10
|
|
|
10
11
|
// --- SECTION IMPORTS ---
|
|
11
12
|
import TemplatesSection from '../../features/styleguide/sections/templates/TemplatesSection';
|
|
@@ -42,6 +43,13 @@ export const docsSitemap: ExtendedSitemapEntry[] = [
|
|
|
42
43
|
icon: 'bot',
|
|
43
44
|
component: AiChat,
|
|
44
45
|
},
|
|
46
|
+
{
|
|
47
|
+
id: 'edge-telemetry',
|
|
48
|
+
path: 'edge-telemetry',
|
|
49
|
+
title: 'Edge Telemetry',
|
|
50
|
+
icon: 'activity',
|
|
51
|
+
component: EdgeTelemetryDemo,
|
|
52
|
+
},
|
|
45
53
|
{
|
|
46
54
|
id: 'styleguide',
|
|
47
55
|
path: 'styleguide',
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
Sidebar,
|
|
5
5
|
type SidebarItem,
|
|
6
6
|
type IconName,
|
|
7
|
-
// 🟢 ADD: ButtonItem
|
|
8
7
|
ButtonItem
|
|
9
8
|
} from '@ramme-io/ui';
|
|
10
9
|
import { settingsSitemap } from './settings.sitemap';
|
|
@@ -13,7 +12,7 @@ import { useAuth } from '../../features/auth/AuthContext';
|
|
|
13
12
|
import { SitemapProvider } from '@ramme-io/kernel';
|
|
14
13
|
import PageTitleUpdater from '../../components/PageTitleUpdater';
|
|
15
14
|
import TemplateSwitcher from '../../components/TemplateSwitcher';
|
|
16
|
-
import { useAppConfig } from '../../features/config/AppConfigContext';
|
|
15
|
+
import { useAppConfig } from '../../features/config/AppConfigContext';
|
|
17
16
|
|
|
18
17
|
const SettingsLayout: React.FC = () => {
|
|
19
18
|
const navigate = useNavigate();
|
|
@@ -31,32 +30,15 @@ const SettingsLayout: React.FC = () => {
|
|
|
31
30
|
|
|
32
31
|
// --- Data Mapping ---
|
|
33
32
|
const sidebarItems: SidebarItem[] = useMemo(() => {
|
|
34
|
-
//
|
|
35
|
-
|
|
33
|
+
// We now only map the actual navigation items from the sitemap.
|
|
34
|
+
// Logout and Reset have been moved to the User Menu below.
|
|
35
|
+
return settingsSitemap.map((item) => ({
|
|
36
36
|
id: item.id,
|
|
37
37
|
label: item.title,
|
|
38
38
|
icon: (item.icon as IconName) || 'settings',
|
|
39
39
|
href: item.path ? `/settings/${item.path}` : '/settings',
|
|
40
40
|
}));
|
|
41
|
-
|
|
42
|
-
// 2. System Actions (Kept in sidebar list for visibility in Settings context)
|
|
43
|
-
const actionItems: SidebarItem[] = [
|
|
44
|
-
{
|
|
45
|
-
id: 'action-reset',
|
|
46
|
-
label: 'Reset Data',
|
|
47
|
-
icon: 'refresh-cw' as IconName,
|
|
48
|
-
onClick: handleResetData,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: 'action-logout',
|
|
52
|
-
label: 'Logout',
|
|
53
|
-
icon: 'log-out' as IconName,
|
|
54
|
-
onClick: logout,
|
|
55
|
-
}
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
return [...navItems, ...actionItems];
|
|
59
|
-
}, [logout]);
|
|
41
|
+
}, []);
|
|
60
42
|
|
|
61
43
|
const activeItemId = useMemo(() => {
|
|
62
44
|
const active = sidebarItems.find(item =>
|
|
@@ -84,9 +66,9 @@ const SettingsLayout: React.FC = () => {
|
|
|
84
66
|
email: user.email,
|
|
85
67
|
avatarUrl: undefined
|
|
86
68
|
} : undefined}
|
|
87
|
-
// ✅
|
|
69
|
+
// ✅ Handles the logout action from the profile menu
|
|
88
70
|
onLogout={logout}
|
|
89
|
-
// ✅
|
|
71
|
+
// ✅ Keeps the secondary actions tucked away in the user menu
|
|
90
72
|
userMenuChildren={
|
|
91
73
|
<>
|
|
92
74
|
<ButtonItem
|
|
@@ -94,6 +76,11 @@ const SettingsLayout: React.FC = () => {
|
|
|
94
76
|
label="Documents"
|
|
95
77
|
onClick={() => navigate('/docs')}
|
|
96
78
|
/>
|
|
79
|
+
<ButtonItem
|
|
80
|
+
icon="cpu"
|
|
81
|
+
label="Bodewell"
|
|
82
|
+
onClick={() => navigate('/bodewell/fleet')}
|
|
83
|
+
/>
|
|
97
84
|
<ButtonItem
|
|
98
85
|
icon="refresh-cw"
|
|
99
86
|
label="Reset Data"
|
|
@@ -119,7 +106,7 @@ const SettingsLayout: React.FC = () => {
|
|
|
119
106
|
/>
|
|
120
107
|
|
|
121
108
|
<div className="flex flex-col flex-1 overflow-hidden">
|
|
122
|
-
<main className="flex-1 overflow-y-auto p-8 bg-muted/20">
|
|
109
|
+
<main id="main-scroll-container" className="flex-1 overflow-y-auto p-8 bg-muted/20">
|
|
123
110
|
<Outlet />
|
|
124
111
|
</main>
|
|
125
112
|
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
package/template/vite.config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
1
2
|
import { defineConfig } from 'vite';
|
|
2
3
|
import react from '@vitejs/plugin-react';
|
|
3
4
|
import path from 'path';
|
|
@@ -78,6 +79,12 @@ export default defineConfig(() => {
|
|
|
78
79
|
alias: aliases,
|
|
79
80
|
},
|
|
80
81
|
|
|
82
|
+
test: {
|
|
83
|
+
globals: true,
|
|
84
|
+
environment: 'jsdom',
|
|
85
|
+
setupFiles: './src/test/setup.ts',
|
|
86
|
+
},
|
|
87
|
+
|
|
81
88
|
optimizeDeps: {
|
|
82
89
|
include: [
|
|
83
90
|
'recharts',
|