MAESTRO: Create separate Vite config for web interface build

- Add vite.config.web.mts with dedicated web interface configuration
- Configure build output to dist/web/ directory
- Set up code splitting with React in separate chunk
- Enable source maps for development debugging
- Add dev server proxy for API and WebSocket connections
- Create index.html entry point with mobile-optimized meta tags
- Create main.tsx with device detection for mobile/desktop routing
- Add index.css with Tailwind, CSS custom properties, and animations
- Create placeholder mobile/desktop App components for Phase 1/2
- Update tailwind.config.mjs to include src/web files
This commit is contained in:
Pedram Amini
2025-11-27 03:30:46 -06:00
parent 1c21f7b8bb
commit 5595157bb8
7 changed files with 886 additions and 0 deletions

151
src/web/desktop/App.tsx Normal file
View File

@@ -0,0 +1,151 @@
/**
* Maestro Desktop Web App
*
* Full-featured collaborative interface for hackathons and team coding.
* Provides real-time collaboration, full visibility, and shared editing.
*
* Phase 2 implementation will expand this component.
*/
import React from 'react';
import { useThemeColors } from '../components/ThemeProvider';
export default function DesktopApp() {
const colors = useThemeColors();
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100vh',
backgroundColor: colors.background,
color: colors.textMain,
}}
>
{/* Header */}
<header
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 24px',
borderBottom: `1px solid ${colors.border}`,
backgroundColor: colors.surface,
}}
>
<h1 style={{ fontSize: '20px', fontWeight: 600 }}>
Maestro Web
</h1>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '14px',
color: colors.textMuted,
}}
>
<span
style={{
width: '10px',
height: '10px',
borderRadius: '50%',
backgroundColor: colors.warning,
}}
/>
Connecting...
</div>
</div>
</header>
{/* Main content */}
<main
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '40px',
}}
>
<div
style={{
textAlign: 'center',
maxWidth: '600px',
}}
>
<div
style={{
marginBottom: '32px',
padding: '32px',
borderRadius: '16px',
backgroundColor: colors.surface,
border: `1px solid ${colors.border}`,
}}
>
<h2 style={{ fontSize: '24px', marginBottom: '16px' }}>
Desktop Collaborative Interface
</h2>
<p
style={{
fontSize: '16px',
color: colors.textMuted,
lineHeight: 1.6,
}}
>
Full-featured web interface for team collaboration during
hackathons and pair programming sessions. Share your AI coding
sessions with teammates in real-time.
</p>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '16px',
marginBottom: '32px',
}}
>
{[
{ title: 'Real-time Sync', desc: 'See changes instantly' },
{ title: 'Multi-user', desc: 'Collaborate together' },
{ title: 'Full History', desc: 'Complete session logs' },
].map(({ title, desc }) => (
<div
key={title}
style={{
padding: '16px',
borderRadius: '8px',
backgroundColor: colors.surface,
border: `1px solid ${colors.border}`,
}}
>
<h3 style={{ fontSize: '14px', marginBottom: '4px' }}>
{title}
</h3>
<p style={{ fontSize: '12px', color: colors.textMuted }}>
{desc}
</p>
</div>
))}
</div>
<p style={{ fontSize: '14px', color: colors.textMuted }}>
This interface will be implemented in Phase 2.
<br />
Make sure Maestro desktop app is running to connect.
</p>
</div>
</main>
</div>
);
}

279
src/web/index.css Normal file
View File

@@ -0,0 +1,279 @@
/**
* Maestro Web Interface Styles
*
* Base styles and CSS custom properties for the web interface.
* Theme colors are injected via JavaScript from ThemeProvider.
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
/* CSS Custom Properties - Default values (overridden by theme) */
:root {
/* Colors */
--color-background: #1a1a2e;
--color-surface: #16213e;
--color-surface-elevated: #1f2e54;
--color-border: #2d3a5a;
--color-text-main: #eaeaea;
--color-text-muted: #8892b0;
--color-accent: #4a9eff;
--color-accent-hover: #3a8eef;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
/* Typography */
--font-mono: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 9999px;
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 200ms ease;
--transition-slow: 300ms ease;
/* Z-Index layers */
--z-dropdown: 100;
--z-sticky: 200;
--z-modal-backdrop: 300;
--z-modal: 400;
--z-toast: 500;
}
/* Base styles */
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
}
body {
font-family: var(--font-mono);
background-color: var(--color-background);
color: var(--color-text-main);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Selection styling */
::selection {
background-color: var(--color-accent);
color: white;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-surface);
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-muted);
}
/* Focus styles */
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Remove default focus outline (we use focus-visible) */
:focus:not(:focus-visible) {
outline: none;
}
/* Link styles */
a {
color: var(--color-accent);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--color-accent-hover);
}
/* Button reset */
button {
font-family: inherit;
font-size: inherit;
cursor: pointer;
background: none;
border: none;
padding: 0;
}
/* Input reset */
input,
textarea,
select {
font-family: inherit;
font-size: inherit;
background: none;
border: none;
}
/* Mobile-specific styles */
@media (max-width: 767px) {
/* Prevent text size adjustment */
html {
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
/* Larger tap targets */
button,
a,
input,
select,
textarea {
min-height: 44px;
}
/* Disable pull-to-refresh on body (we'll handle it ourselves) */
body {
overscroll-behavior-y: contain;
}
}
/* Safe area insets for iOS */
@supports (padding-top: env(safe-area-inset-top)) {
.safe-area-top {
padding-top: env(safe-area-inset-top);
}
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
.safe-area-left {
padding-left: env(safe-area-inset-left);
}
.safe-area-right {
padding-right: env(safe-area-inset-right);
}
}
/* Animation keyframes */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Utility classes */
.animate-spin {
animation: spin 1s linear infinite;
}
.animate-pulse {
animation: pulse 2s ease-in-out infinite;
}
.animate-fadeIn {
animation: fadeIn var(--transition-normal);
}
.animate-slideUp {
animation: slideUp var(--transition-normal);
}
.animate-slideDown {
animation: slideDown var(--transition-normal);
}
/* Hide scrollbar but keep functionality */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Truncate text */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Line clamp */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

62
src/web/index.html Normal file
View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#1a1a2e" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="description" content="Maestro Web Interface - Remote control for your AI coding assistants" />
<title>Maestro Web</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
/* Critical CSS for initial load */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body, #root {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--color-background, #1a1a2e);
color: var(--color-text-main, #eaeaea);
}
/* Loading state */
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-main, #eaeaea);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-border, #333);
border-top-color: var(--color-accent, #4a9eff);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="root">
<div class="loading">
<div class="loading-spinner"></div>
</div>
</div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

140
src/web/main.tsx Normal file
View File

@@ -0,0 +1,140 @@
/**
* Maestro Web Interface Entry Point
*
* This is the main entry point for the web interface.
* It detects the device type and renders the appropriate interface.
*/
import React, { StrictMode, lazy, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import { ThemeProvider } from './components/ThemeProvider';
import './index.css';
// Lazy load mobile and desktop apps for code splitting
// These will be created in Phase 1 and Phase 2
const MobileApp = lazy(() =>
import('./mobile/App').catch(() => ({
default: () => <PlaceholderApp type="mobile" />,
}))
);
const DesktopApp = lazy(() =>
import('./desktop/App').catch(() => ({
default: () => <PlaceholderApp type="desktop" />,
}))
);
/**
* Detect if the device is mobile based on screen size and touch capability
*/
function isMobileDevice(): boolean {
// Check for touch capability
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// Check screen width (768px is a common breakpoint)
const isSmallScreen = window.innerWidth < 768;
// Check user agent for mobile indicators
const mobileUserAgent = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
// Consider mobile if small screen OR (has touch AND mobile user agent)
return isSmallScreen || (hasTouch && mobileUserAgent);
}
/**
* Placeholder component shown while the actual app loads
* or if the app module hasn't been created yet
*/
function PlaceholderApp({ type }: { type: 'mobile' | 'desktop' }) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
padding: '20px',
textAlign: 'center',
color: 'var(--color-text-main)',
backgroundColor: 'var(--color-background)',
}}
>
<h1 style={{ marginBottom: '16px', fontSize: '24px' }}>Maestro Web</h1>
<p style={{ marginBottom: '8px', color: 'var(--color-text-muted)' }}>
{type === 'mobile' ? 'Mobile' : 'Desktop'} interface coming soon
</p>
<p style={{ fontSize: '14px', color: 'var(--color-text-muted)' }}>
Connect to your Maestro desktop app to get started
</p>
</div>
);
}
/**
* Loading fallback component
*/
function LoadingFallback() {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
backgroundColor: 'var(--color-background)',
}}
>
<div
style={{
width: '40px',
height: '40px',
border: '3px solid var(--color-border)',
borderTopColor: 'var(--color-accent)',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
}}
/>
</div>
);
}
/**
* Main App component that routes to mobile or desktop
*/
function App() {
const [isMobile, setIsMobile] = React.useState(isMobileDevice);
// Re-check on resize (for responsive design testing)
React.useEffect(() => {
const handleResize = () => {
setIsMobile(isMobileDevice());
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<ThemeProvider>
<Suspense fallback={<LoadingFallback />}>
{isMobile ? <MobileApp /> : <DesktopApp />}
</Suspense>
</ThemeProvider>
);
}
// Mount the application
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(
<StrictMode>
<App />
</StrictMode>
);
} else {
console.error('Root element not found');
}

142
src/web/mobile/App.tsx Normal file
View File

@@ -0,0 +1,142 @@
/**
* Maestro Mobile Web App
*
* Lightweight remote control interface for mobile devices.
* Focused on quick command input and session monitoring.
*
* Phase 1 implementation will expand this component.
*/
import React from 'react';
import { useThemeColors } from '../components/ThemeProvider';
export default function MobileApp() {
const colors = useThemeColors();
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100vh',
backgroundColor: colors.background,
color: colors.textMain,
}}
>
{/* Header */}
<header
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
borderBottom: `1px solid ${colors.border}`,
backgroundColor: colors.surface,
}}
className="safe-area-top"
>
<h1 style={{ fontSize: '18px', fontWeight: 600 }}>Maestro</h1>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '12px',
color: colors.textMuted,
}}
>
<span
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: colors.warning,
}}
/>
Connecting...
</div>
</header>
{/* Main content area */}
<main
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
textAlign: 'center',
}}
>
<div
style={{
marginBottom: '24px',
padding: '16px',
borderRadius: '12px',
backgroundColor: colors.surface,
border: `1px solid ${colors.border}`,
maxWidth: '300px',
}}
>
<h2 style={{ fontSize: '16px', marginBottom: '8px' }}>
Mobile Remote Control
</h2>
<p style={{ fontSize: '14px', color: colors.textMuted }}>
Send commands to your AI assistants from anywhere. This interface
will be implemented in Phase 1.
</p>
</div>
<p style={{ fontSize: '12px', color: colors.textMuted }}>
Make sure Maestro desktop app is running
</p>
</main>
{/* Bottom input bar placeholder */}
<footer
style={{
padding: '12px 16px',
borderTop: `1px solid ${colors.border}`,
backgroundColor: colors.surface,
}}
className="safe-area-bottom"
>
<div
style={{
display: 'flex',
gap: '8px',
alignItems: 'center',
}}
>
<input
type="text"
placeholder="Enter command..."
disabled
style={{
flex: 1,
padding: '12px 16px',
borderRadius: '8px',
backgroundColor: colors.background,
border: `1px solid ${colors.border}`,
color: colors.textMuted,
fontSize: '14px',
}}
/>
<button
disabled
style={{
padding: '12px 16px',
borderRadius: '8px',
backgroundColor: colors.accent,
color: '#fff',
opacity: 0.5,
fontSize: '14px',
}}
>
Send
</button>
</div>
</footer>
</div>
);
}

View File

@@ -2,6 +2,7 @@
export default {
content: [
"./src/renderer/**/*.{js,ts,jsx,tsx}",
"./src/web/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {

111
vite.config.web.mts Normal file
View File

@@ -0,0 +1,111 @@
/**
* Vite configuration for Maestro Web Interface
*
* This config builds the web interface (both mobile and desktop)
* as a standalone bundle that can be served by the Fastify server.
*
* Output: dist/web/
*/
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import { readFileSync } from 'fs';
// Read version from package.json
const packageJson = JSON.parse(
readFileSync(path.join(__dirname, 'package.json'), 'utf-8')
);
const appVersion = process.env.VITE_APP_VERSION || packageJson.version;
export default defineConfig({
plugins: [react()],
// Entry point for web interface
root: path.join(__dirname, 'src/web'),
// Use relative paths for assets (served from Fastify)
base: './',
define: {
__APP_VERSION__: JSON.stringify(appVersion),
},
resolve: {
alias: {
// Allow importing from renderer types/constants
'@renderer': path.join(__dirname, 'src/renderer'),
'@web': path.join(__dirname, 'src/web'),
'@shared': path.join(__dirname, 'src/shared'),
},
},
build: {
outDir: path.join(__dirname, 'dist/web'),
emptyOutDir: true,
// Generate source maps for debugging
sourcemap: true,
rollupOptions: {
input: {
// Single entry point that handles routing to mobile/desktop
main: path.join(__dirname, 'src/web/index.html'),
},
output: {
// Organize output by type
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
// Manual chunking for better caching
manualChunks: {
// React core in its own chunk
react: ['react', 'react-dom'],
},
},
},
// Target modern browsers (web interface doesn't need legacy support)
target: 'es2020',
// Minimize bundle size
minify: 'esbuild',
// Report chunk sizes
reportCompressedSize: true,
},
// Development server (for testing web interface standalone)
server: {
port: 5174, // Different from renderer dev server (5173)
strictPort: true,
// Proxy API calls to the running Maestro app during development
proxy: {
'/api': {
target: 'http://localhost:45678',
changeOrigin: true,
},
'/ws': {
target: 'ws://localhost:45678',
ws: true,
},
},
},
// Preview server for testing production build
preview: {
port: 5175,
strictPort: true,
},
// Enable CSS code splitting
css: {
devSourcemap: true,
},
// Optimize dependencies
optimizeDeps: {
include: ['react', 'react-dom'],
},
});