// Main app shell — sidebar, topbar, page routing, drawer, tour, confetti const { useState, useEffect, useRef, useCallback } = React; const DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "light", "accent": "#ff4400", "density": "comfortable", "fontPair": "jetbrains", "rightPane": "readme", "demoValidation": "auto", "demoPhase": "auto", "showTour": false, "page": "create" }/*EDITMODE-END*/; const FONT_PAIRS = { jetbrains: { display: "'Archivo Black', sans-serif", mono: "'JetBrains Mono', ui-monospace, monospace" }, bebas: { display: "'Bebas Neue', sans-serif", mono: "'Space Mono', ui-monospace, monospace" }, anton: { display: "'Anton', sans-serif", mono: "'JetBrains Mono', ui-monospace, monospace" }, allmono: { display: "'JetBrains Mono', ui-monospace, monospace", mono: "'JetBrains Mono', ui-monospace, monospace" }, }; // Restore theme from localStorage so it doesn't flash (function() { const saved = localStorage.getItem('devex-theme'); if (saved) document.documentElement.dataset.theme = saved; })(); function App() { const I = window.I; const { ACTIVITY } = window.DEVEX_DATA; const [tweaks, setTweak] = window.useTweaks({ ...DEFAULTS, theme: localStorage.getItem('devex-theme') || DEFAULTS.theme, }); // Server-provided user (null if not logged in) const serverUser = window.DEVEX_INIT?.user || null; const [loggedIn, setLoggedIn] = useState(!!serverUser); const userLogin = serverUser?.login || 'user'; const avatarLetter = userLogin.charAt(0).toUpperCase(); useEffect(() => { const root = document.documentElement; root.dataset.theme = tweaks.theme; localStorage.setItem('devex-theme', tweaks.theme); root.style.setProperty('--accent', tweaks.accent); root.style.setProperty('--accent-tint', hexToTint(tweaks.accent, 0.10)); root.style.setProperty('--density', tweaks.density === 'compact' ? '0.78' : '1'); const fp = FONT_PAIRS[tweaks.fontPair] || FONT_PAIRS.jetbrains; root.style.setProperty('--display', fp.display); root.style.setProperty('--mono', fp.mono); }, [tweaks]); const [readmeOpen, setReadmeOpen] = useState(false); const [tourOpen, setTourOpen] = useState(false); const [collapsed, setCollapsed] = useState(false); const [feedbackOpen, setFeedbackOpen] = useState(false); useEffect(() => { if (tweaks.showTour) setTourOpen(true); }, [tweaks.showTour]); const fireConfetti = useCallback(() => confettiBurst(tweaks.accent), [tweaks.accent]); const handleLogout = () => { window.location.href = '/auth/logout'; }; if (!loggedIn) return { window.location.href = '/auth/login'; }} />; return (
{/* Sidebar */} {/* Main */}
workspace / {tweaks.page === 'create' ? 'new repository' : tweaks.page}
github · connected
{tweaks.page === 'create' && ( <>

New
repository.

// compose · push · deploy

setReadmeOpen(true)} onTriggerConfetti={fireConfetti} />
)} {tweaks.page === 'repos' && } {tweaks.page === 'updates' && } {tweaks.page === 'templates' && } {tweaks.page === 'deployments' && }
{/* Drawer */} setReadmeOpen(false)} /> {/* Tour */} {tourOpen && { setTourOpen(false); setTweak('showTour', false); }} />} {/* Feedback */} {feedbackOpen && setFeedbackOpen(false)} />} {/* Mobile nav */} setTweak('page', v)} /> {/* Tweaks */} setTweak('page', v)} /> setTweak('theme', v)} /> setTweak('accent', v)} /> setTweak('density', v)} /> setTweak('fontPair', v)} /> setTweak('rightPane', v)} /> setTweak('demoValidation', v)} /> setTweak('demoPhase', v)} /> setTweak('showTour', true)}>Show onboarding tour confettiBurst(tweaks.accent)}>Trigger confetti 🎉
); } // ---------- Right pane ---------- function RightPane({ tweaks, setTweak }) { const I = window.I; const view = tweaks.rightPane; return (
{view === 'readme' ? 'auto-generated' : 'last 24h'}
{view === 'readme' && } {view === 'activity' && }
); } function ReadmeContent() { const { SERVICES } = window.DEVEX_DATA; const svcList = SERVICES.slice(0, 3); const userLogin = window.DEVEX_INIT?.user?.login || 'org'; return (

your-repo-name

DevEx VPS Traefik {svcList.map(s => {s.name})}

A scaffolded application bundle generated by DevEx. Includes routing, optional database, and a deployable app wired through docker compose.

Quickstart

{`git clone git@github.com:${userLogin}/your-repo.git\ncd your-repo\nmake up`}

Services

Deploy

Push to main. The included GitHub Actions workflow builds and rolls a zero-downtime swap on your VPS.

); } function ActivityFeed() { const I = window.I; const { ACTIVITY } = window.DEVEX_DATA; return (
{ACTIVITY.map((a, i) => { const Glyph = I[a.kind] || I.GitBranch; return (
{a.t}
{a.text}
); })}
); } // ---------- Drawer ---------- function ReadmeDrawer({ open, onClose }) { const I = window.I; return ( <>
README · Live Preview
); } // ---------- Tour ---------- const TOUR_STEPS = [ { sel: '[data-tour="tabs"]', title: 'TWO PATHS', body: 'Spin up a full multi-service VPS app, or fork from a curated repo template. Switch any time.' }, { sel: '[data-tour="name"]', title: 'NAME IT', body: 'As you type, DevEx checks GitHub in real time. Border turns green when available. Hit Shuffle for an instant name.' }, { sel: '[data-tour="services"]', title: 'COMPOSE', body: 'Click any service to add it. Each card generates docker-compose entries and Traefik routes automatically.' }, { sel: '[data-tour="submit"]', title: 'ONE COMMAND', body: 'Push the composed repo to GitHub with topics applied.' }, ]; function Tour({ onClose }) { const [step, setStep] = useState(0); const [rect, setRect] = useState(null); useEffect(() => { const place = () => { const el = document.querySelector(TOUR_STEPS[step].sel); if (!el) { setRect(null); return; } const r = el.getBoundingClientRect(); setRect({ x: r.left, y: r.top, w: r.width, h: r.height }); }; place(); window.addEventListener('resize', place); const t = setInterval(place, 250); return () => { window.removeEventListener('resize', place); clearInterval(t); }; }, [step]); if (!rect) return null; const pad = 14, popHeight = 200; const placeBelow = rect.y + rect.h + pad + popHeight < window.innerHeight; const popY = placeBelow ? rect.y + rect.h + pad : Math.max(20, rect.y - pad - popHeight); const popX = Math.min(window.innerWidth - 340, Math.max(20, rect.x)); const next = () => step < TOUR_STEPS.length - 1 ? setStep(s => s + 1) : onClose(); const prev = () => setStep(s => Math.max(0, s - 1)); return ( <>
{String(step + 1).padStart(2,'0')} / {String(TOUR_STEPS.length).padStart(2,'0')} onboarding
{TOUR_STEPS[step].title} {TOUR_STEPS[step].body}
{step > 0 && }
); } // ---------- Feedback Modal ---------- function FeedbackModal({ page, onClose }) { const I = window.I; const [category, setCategory] = useState('idea'); const [comment, setComment] = useState(''); const [sent, setSent] = useState(false); const send = () => { if (!comment.trim()) return; setSent(true); setTimeout(() => onClose(), 1400); }; const cats = [ { id: 'bug', label: 'Bug', sym: '✕', desc: 'Something is broken' }, { id: 'idea', label: 'Idea', sym: '+', desc: 'Suggest an improvement' }, { id: 'praise', label: 'Praise', sym: '✓', desc: 'Tell us what works' }, ]; return ( <>
Send feedback
{sent ? (
Thanks · received
// logged to platform-eng/feedback
) : (
Category
{cats.map((c, i) => ( ))}
Comment page · {page}