Generated by Spark: Create a cloud-based game development IDE called "AeThex Studio" using Next.js and Tailwind.

Layout:

Top navbar: AeThex logo (left), project name (center), user menu with avatar (right)
Left sidebar (250px): File tree navigator with folders/files, "New File" and "New Folder" buttons
Center: Code editor area (use Monaco Editor component if available, or textarea with syntax highlighting)
Right sidebar (300px, collapsible): AI Assistant chat panel with message history
Bottom panel (200px, collapsible): Console output and terminal
Features:

File tree supports create/rename/delete operations
Tabs for multiple open files
Syntax highlighting for Lua, JavaScript, TypeScript
Dark theme with purple/cyan accent colors (futuristic gaming aesthetic)
Split panes are resizable
Auto-save indicator in navbar
Use TypeScript and proper state management. Make it feel like VS Code but purpose-built for game development.

Create a unified game development workspace that shows the same game running on three platforms simultaneously:

Left panel: Roblox viewport (embed mockup)
Center panel: Web browser viewport with game canvas
Right panel: Mobile device mockup with game
Include:

Shared state indicator showing synced variables (player position, score, inventory)
Platform-specific code tabs that show Lua (Roblox), JavaScript (Web), and React Native (Mobile)
"Deploy to Platform" buttons for each
Sync status indicator (green = synced, yellow = syncing, red = conflict)
Console output panel at bottom showing platform-specific logs
Use a dark IDE-like theme. Make it feel like a professional developer tool.

Add a "New Project" modal to the AeThex Studio IDE:

Modal triggered by "New Project" button in navbar:

Step 1: Choose template (card grid layout)

Roblox Game Starter
Cross-Platform Multiplayer
Transmedia Story Project
Blank Project Each card shows icon, title, description, and "Popular" badge where applicable
Step 2: Project configuration form

Project name input
Platform checkboxes (Roblox, Web, Mobile)
Enable Nexus Engine toggle (with tooltip explaining state sync)
Enable Passport Auth toggle (with tooltip explaining unified identity)
Step 3: Review summary and "Create Project" button

After creation, populate file tree with template structure and open main file in editor.

Use a stepper UI component to show progress. Modern, clean design.

Build the AI Assistant chat panel for the right sidebar:

UI Components:

Header: "AeThex AI Assistant" with model selector dropdown (Claude Sonnet, GPT-4o)
Chat message history (scrollable, auto-scroll to bottom)
Messages alternate left (user) and right (AI) with avatars
Code blocks in messages with syntax highlighting and copy button
Input textarea at bottom with "Send" button
Token usage indicator (shows X / 500K tokens used this month)
Quick Actions (buttons above input):

"Explain selected code"
"Add comments"
"Convert to cross-platform"
"Generate tests"
Message Types:

User messages: blue background
AI messages: dark gray background with purple border
System messages: small, centered, gray text (e.g., "Code inserted into editor")
Features:

When AI generates code, show "Insert into editor" button
Track conversation context (include current file, project structure)
Show typing indicator when AI is responding
Make the UX smooth and polished.

Create a preview panel modal that opens when user clicks "Preview" in navbar:

Layout (fullscreen modal with dark backdrop):

Close button (top right)
Tab navigation: "Roblox" | "Web" | "Mobile" | "All Platforms"
"All Platforms" tab shows three columns:

Left: Roblox viewport (iframe placeholder with Roblox logo)
Center: Web browser viewport (iframe with game canvas)
Right: Mobile device mockup (iPhone frame with game view)
Shared State Indicator (floating panel at bottom):

Shows synced variables in real-time table: Variable | Roblox | Web | Mobile | Status playerX | 120 | 120 | 120 | ✓ Synced health | 100 | 98 | 100 | ⚠ Syncing
Color coded: Green (synced), Yellow (syncing), Red (conflict)
Controls:

"Refresh All" button
Individual "Deploy to [Platform]" buttons
Latency simulator slider (0-500ms)
Use glassmorphism UI style with blur effects. This should look futuristic and impressive.

Add user authentication and workspace management:

Login page:

AeThex logo and tagline
"Sign in with AeThex Passport" button (purple gradient)
"Sign in with Google" button
"Sign in with GitHub" button
Illustration of cross-platform game development
Dashboard (after login, before opening project):

Header: "My Workspaces" with "New Workspace" button
Grid of workspace cards showing:
Project name
Last modified timestamp
Platform badges (Roblox, Web, Mobile icons)
Thumbnail/preview image
Three-dot menu (Rename, Delete, Share)
Empty state if no workspaces: "Create your first game" CTA with template previews
Each workspace card is clickable and opens the IDE with that project loaded.

Use modern card design with hover effects. Show loading skeletons while fetching data.
This commit is contained in:
Anderson 2026-01-17 03:02:17 +00:00 committed by GitHub
parent fb17d0e075
commit 8d6ebeba35
8 changed files with 1413 additions and 90 deletions

164
PRD.md
View file

@ -1,61 +1,70 @@
# Planning Guide # Planning Guide
AeThex Studio is a browser-based Roblox Lua script editor with integrated AI assistance for rapid prototyping and learning. AeThex Studio is a browser-based cross-platform game development IDE with file management, multi-platform preview, AI assistance, and project templates.
**Experience Qualities**: **Experience Qualities**:
1. **Professional** - Should feel like a legitimate development tool with clean typography and purposeful spacing 1. **Professional** - Should feel like a complete IDE with file navigation, tabs, and split panels like VS Code
2. **Intelligent** - AI assistance should feel seamlessly integrated, not bolted on 2. **Intelligent** - AI assistance deeply integrated with context awareness and code suggestions
3. **Empowering** - Users should feel confident writing and understanding Lua code 3. **Cross-Platform** - Unified workspace showing Roblox, Web, and Mobile development simultaneously
**Complexity Level**: Light Application (multiple features with basic state) **Complexity Level**: Complex Application (advanced functionality with multiple views and state management)
This is a focused code editor with AI chat, script templates, and syntax validation - not a full-featured IDE with file systems, debugging, or deployment pipelines. This is now a full-featured IDE with file tree navigation, multiple open files, tabbed editor, AI chat panel, console output, multi-platform preview, and project templates - creating a complete game development workspace.
## Essential Features ## Essential Features
### Monaco Code Editor ### File Tree Navigator
- **Functionality**: Full-featured Lua code editor with syntax highlighting, autocomplete, and line numbers - **Functionality**: Left sidebar showing folder/file hierarchy with create, rename, and delete operations
- **Purpose**: Provide a professional coding environment for Roblox scripting - **Purpose**: Organize multiple scripts and manage project structure like a real IDE
- **Trigger**: Loads on app start with default template - **Trigger**: Always visible on desktop, accessible via tabs on mobile
- **Progression**: User opens app → sees editor with starter code → begins typing → receives syntax highlighting and autocomplete - **Progression**: User clicks "New File" → enters name → new file appears in tree → clicks to open
- **Success criteria**: Code is editable, syntax-highlighted, and changes persist between sessions - **Success criteria**: Files persist between sessions, operations work smoothly, selected file highlights
### AI Chat Assistant ### Multi-File Tabs
- **Functionality**: Conversational AI panel that helps explain code, debug issues, and suggest improvements - **Functionality**: Horizontal tabs showing open files with close buttons
- **Purpose**: Lower the learning curve for new Roblox developers - **Purpose**: Work with multiple scripts simultaneously without losing context
- **Trigger**: User clicks chat button or types a question in the AI panel - **Trigger**: Opening a file from tree adds a tab
- **Progression**: User asks question → AI analyzes current code → provides contextual answer with code examples - **Progression**: User opens file → tab appears → clicks tab to switch → clicks X to close
- **Success criteria**: AI responses are relevant, helpful, and include code snippets when appropriate - **Success criteria**: Active tab highlighted, tab state persists, smooth switching
### Script Templates ### Multi-Platform Preview
- **Functionality**: Pre-built Roblox script templates (basic part manipulation, player join events, GUI interactions, etc.) - **Functionality**: Modal showing Roblox, Web, and Mobile viewports side-by-side with shared state sync
- **Purpose**: Jump-start development and teach common patterns - **Purpose**: Visualize how game runs across all platforms simultaneously
- **Trigger**: User clicks "Templates" button - **Trigger**: User clicks "Preview" button in toolbar
- **Progression**: User browses templates → clicks template → code loads into editor → user modifies for their needs - **Progression**: Click Preview → modal opens → see three viewport mockups → view synced state table → close modal
- **Success criteria**: Templates load instantly and represent common Roblox use cases - **Success criteria**: Modal is visually impressive with glassmorphism, state sync table updates, deploy buttons present
### Code Validation ### New Project Wizard
- **Functionality**: Real-time syntax checking and error highlighting - **Functionality**: 3-step modal for creating projects with templates, platform selection, and feature toggles
- **Purpose**: Catch errors before testing in Roblox Studio - **Purpose**: Quick-start new games with appropriate scaffolding
- **Trigger**: Automatic as user types - **Trigger**: User clicks "New Project" button in toolbar
- **Progression**: User types invalid Lua → error highlights appear → user hovers for details → fixes error - **Progression**: Click New Project → choose template → configure settings → review → create → file tree populates
- **Success criteria**: Common Lua syntax errors are caught and clearly displayed - **Success criteria**: Stepper UI shows progress, all template types available, settings persist
### Export/Copy Code ### Console Output Panel
- **Functionality**: One-click copy of current code to clipboard - **Functionality**: Bottom panel showing logs filtered by platform (Roblox/Web/Mobile/System)
- **Purpose**: Easy transfer to Roblox Studio - **Purpose**: Display runtime output and errors from all platforms in one place
- **Trigger**: User clicks copy button - **Trigger**: Always visible on desktop (collapsible), shows logs automatically
- **Progression**: User finishes editing → clicks copy → receives confirmation → pastes into Roblox Studio - **Progression**: Code runs → logs appear → filter by platform → clear logs button
- **Success criteria**: Code copies to clipboard with proper formatting - **Success criteria**: Color-coded messages, platform badges, auto-scroll, clear functionality
### User Profile Menu
- **Functionality**: Avatar dropdown in toolbar showing GitHub profile with sign-out option
- **Purpose**: Display current user identity and provide account actions
- **Trigger**: Click avatar icon in top-right
- **Progression**: User logged in → avatar shows → click opens menu → see profile info
- **Success criteria**: Fetches real GitHub user via spark.user(), displays avatar and email
## Edge Case Handling ## Edge Case Handling
- **Empty Editor**: Show helpful placeholder text with getting started tips - **Empty File Tree**: Show placeholder with "Create your first file" message
- **No Open Files**: Display welcome message in editor area
- **AI Errors**: Display user-friendly error messages if AI service is unavailable - **AI Errors**: Display user-friendly error messages if AI service is unavailable
- **Large Scripts**: Monaco handles large files, but warn if script exceeds 5000 lines (unusual for Roblox scripts) - **Large Scripts**: Monaco handles large files, warn if exceeds 10,000 lines
- **Invalid Lua**: Show errors inline without blocking the user from continuing to type - **Invalid Operations**: Prevent deleting root folder, renaming to empty string
- **Lost Work**: Auto-save code every 30 seconds to prevent data loss - **Network Issues**: Show offline indicator if can't reach AI or user services
- **Mobile Layout**: Stack panels vertically with tab navigation for Files/Editor/AI
## Design Direction ## Design Direction
The design should evoke a sense of technical sophistication meets creative playfulness - this is a tool for building games, not enterprise software. Think dark themes with neon accents, geometric patterns, and smooth transitions that feel modern and game-inspired. The design should evoke a professional game development IDE with futuristic gaming aesthetics - dark themes with vibrant cyan/purple accents, glassmorphism effects in modals, and smooth panel animations. This is VS Code meets Unreal Engine editor with a playful gaming twist.
## Color Selection ## Color Selection
A dark, code-focused theme with vibrant accent colors inspired by Roblox's playful brand and gaming aesthetics. A dark, code-focused theme with vibrant accent colors inspired by Roblox's playful brand and gaming aesthetics.
@ -86,40 +95,49 @@ Animations should feel snappy and technical - quick state transitions with subtl
## Component Selection ## Component Selection
- **Components**: - **Components**:
- `Button` (primary actions like "Run", "Copy", "Ask AI") with custom variants for destructive and accent states - `Button` with accent variant for primary actions (New Project, Preview)
- `Card` for template previews and code snippets - `Card` for template previews, project configs, and preview mockups
- `Separator` for dividing editor from chat panel - `Dialog` for Preview Modal, New Project Modal, About dialog
- `ScrollArea` for chat messages and template list - `DropdownMenu` for file context menus and user profile menu
- `Tabs` for switching between templates and settings - `Avatar` for user profile display
- `Dialog` for first-time user onboarding - `ScrollArea` for file tree, chat messages, console logs
- `Badge` for Lua syntax error indicators - `Tabs` for template categories, preview modes, console filters, mobile navigation
- `Tooltip` for icon button explanations - `Input` for file rename, project name entry
- Custom Monaco Editor wrapper component - `Checkbox` for platform selection in new project
- Custom AI chat component with message bubbles - `Switch` for Nexus Engine and Passport Auth toggles
- `Badge` for popular templates, platform indicators, status labels
- `Tooltip` for button explanations and feature descriptions
- `ResizablePanel` for adjustable sidebar and panel widths
- Custom Monaco Editor wrapper
- Custom FileTree with nested folders
- Custom FileTabs with close buttons
- Custom ConsolePanel with platform filtering
- **Customizations**: - **Customizations**:
- Monaco editor needs dark theme configuration - Monaco editor with dark theme configuration
- Chat bubbles with distinct styling for user vs AI messages - File tree with expand/collapse animations
- Template cards with code preview using syntax-highlighted pre blocks - Preview modal with glassmorphism effects (backdrop-blur)
- Resizable panels using react-resizable-panels - Multi-platform viewport mockups with device frames
- Shared state sync table with color-coded status
- Console with platform-specific badge colors
- **States**: - **States**:
- Buttons: Default (solid accent), Hover (brightened +10% lightness), Active (scale 0.97), Disabled (50% opacity) - Buttons: Accent primary (New Project), Ghost secondary (Templates, Copy)
- Editor: Default (dark background), Focused (subtle glow border) - File items: Selected (accent background), Hover (muted background)
- Chat input: Default (muted border), Focused (accent border with glow) - Tabs: Active (accent bottom border), Inactive (transparent)
- Console logs: Color by type (red error, yellow warn, blue info)
- **Icon Selection**: - **Icon Selection**:
- Code (editor): `<Code />` from Phosphor - File tree: `<Folder />`, `<File />`, `<Plus />`, `<Trash />`, `<PencilSimple />`
- Chat: `<ChatCircle />` from Phosphor - Toolbar: `<FolderPlus />` (New Project), `<Play />` (Preview), `<User />` (Profile)
- Copy: `<Copy />` from Phosphor - Preview: `<ArrowsClockwise />` (Refresh), `<GameController />`, `<Globe />`, `<DeviceMobile />`
- Templates: `<FileCode />` from Phosphor - Console: `<Terminal />` (Console icon)
- AI sparkle: `<Sparkle />` from Phosphor - All from Phosphor Icons
- Play/Run: `<Play />` from Phosphor
- **Spacing**: - **Spacing**:
- Container padding: p-4 (16px) - Container padding: p-4
- Component gaps: gap-4 (16px) for major sections, gap-2 (8px) for grouped items - Panel borders: border-border (1px)
- Button padding: px-6 py-2.5 - Component gaps: gap-4 for sections, gap-2 for grouped items
- Card padding: p-6 - File tree indent: 12px per level
- **Mobile**: - **Mobile**:
- Stack editor and chat vertically on mobile - Tabs for Files/Editor/AI switching on mobile
- Make chat panel collapsible on small screens - File tree full-screen on mobile
- Reduce font sizes: H1 to 24px, body to 13px - Hide console panel on mobile
- Full-width buttons with bottom sheet for templates - Preview modal scrollable on small screens
- Hide Monaco minimap on screens < 768px - Stepper in New Project modal scales down

View file

@ -5,52 +5,260 @@ import { AIChat } from '@/components/AIChat';
import { Toolbar } from '@/components/Toolbar'; import { Toolbar } from '@/components/Toolbar';
import { TemplatesDrawer } from '@/components/TemplatesDrawer'; import { TemplatesDrawer } from '@/components/TemplatesDrawer';
import { WelcomeDialog } from '@/components/WelcomeDialog'; import { WelcomeDialog } from '@/components/WelcomeDialog';
import { FileTree, FileNode } from '@/components/FileTree';
import { FileTabs } from '@/components/FileTabs';
import { PreviewModal } from '@/components/PreviewModal';
import { NewProjectModal, ProjectConfig } from '@/components/NewProjectModal';
import { ConsolePanel } from '@/components/ConsolePanel';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
import { useKV } from '@github/spark/hooks'; import { useKV } from '@github/spark/hooks';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { toast } from 'sonner';
function App() { function App() {
const [currentCode, setCurrentCode] = useState(''); const [currentCode, setCurrentCode] = useState('');
const [showTemplates, setShowTemplates] = useState(false); const [showTemplates, setShowTemplates] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [showNewProject, setShowNewProject] = useState(false);
const [code, setCode] = useKV('aethex-current-code', ''); const [code, setCode] = useKV('aethex-current-code', '');
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [files, setFiles] = useKV<FileNode[]>('aethex-files', [
{
id: 'root',
name: 'src',
type: 'folder',
children: [
{
id: 'file-1',
name: 'script.lua',
type: 'file',
content: `-- Welcome to AeThex Studio!
-- Write your Roblox Lua code here
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined the game!")
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = leaderstats
end)`,
},
],
},
]);
const [openFiles, setOpenFiles] = useKV<FileNode[]>('aethex-open-files', []);
const [activeFileId, setActiveFileId] = useKV<string>('aethex-active-file', 'file-1');
const handleTemplateSelect = (templateCode: string) => { const handleTemplateSelect = (templateCode: string) => {
setCode(templateCode); setCode(templateCode);
setCurrentCode(templateCode); setCurrentCode(templateCode);
}; };
const handleFileSelect = (file: FileNode) => {
if (file.type === 'file') {
setActiveFileId(file.id);
if (!(openFiles || []).find((f) => f.id === file.id)) {
setOpenFiles((prev) => [...(prev || []), file]);
}
setCode(file.content || '');
setCurrentCode(file.content || '');
}
};
const handleFileCreate = (name: string, parentId?: string) => {
const newFile: FileNode = {
id: `file-${Date.now()}`,
name: name.endsWith('.lua') ? name : `${name}.lua`,
type: 'file',
content: '-- New file\n',
};
setFiles((prev) => {
const addToFolder = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === 'root' && !parentId) {
return {
...node,
children: [...(node.children || []), newFile],
};
}
if (node.id === parentId && node.type === 'folder') {
return {
...node,
children: [...(node.children || []), newFile],
};
}
if (node.children) {
return { ...node, children: addToFolder(node.children) };
}
return node;
});
};
return addToFolder(prev || []);
});
toast.success(`Created ${newFile.name}`);
};
const handleFileRename = (id: string, newName: string) => {
setFiles((prev) => {
const rename = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === id) {
return { ...node, name: newName };
}
if (node.children) {
return { ...node, children: rename(node.children) };
}
return node;
});
};
return rename(prev || []);
});
};
const handleFileDelete = (id: string) => {
setFiles((prev) => {
const deleteNode = (nodes: FileNode[]): FileNode[] => {
return nodes.filter((node) => {
if (node.id === id) return false;
if (node.children) {
node.children = deleteNode(node.children);
}
return true;
});
};
return deleteNode(prev || []);
});
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
if (activeFileId === id) {
setActiveFileId((openFiles || [])[0]?.id || '');
}
};
const handleFileClose = (id: string) => {
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
if (activeFileId === id) {
const remaining = (openFiles || []).filter((f) => f.id !== id);
setActiveFileId(remaining[0]?.id || '');
}
};
const handleCreateProject = (config: ProjectConfig) => {
const projectFiles: FileNode[] = [
{
id: 'root',
name: config.name,
type: 'folder',
children: [
{
id: `file-${Date.now()}`,
name: 'main.lua',
type: 'file',
content: `-- ${config.name}\n-- Template: ${config.template}\n\nprint("Project initialized!")`,
},
],
},
];
setFiles(projectFiles);
setOpenFiles([]);
setActiveFileId('');
};
return ( return (
<div className="h-screen flex flex-col bg-background text-foreground"> <div className="h-screen flex flex-col bg-background text-foreground">
<Toolbar code={currentCode} onTemplatesClick={() => setShowTemplates(true)} /> <Toolbar
code={currentCode}
onTemplatesClick={() => setShowTemplates(true)}
onPreviewClick={() => setShowPreview(true)}
onNewProjectClick={() => setShowNewProject(true)}
/>
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden flex flex-col">
{isMobile ? ( {isMobile ? (
<Tabs defaultValue="editor" className="h-full flex flex-col"> <Tabs defaultValue="editor" className="h-full flex flex-col">
<TabsList className="w-full rounded-none border-b border-border"> <TabsList className="w-full rounded-none border-b border-border">
<TabsTrigger value="files" className="flex-1">Files</TabsTrigger>
<TabsTrigger value="editor" className="flex-1">Editor</TabsTrigger> <TabsTrigger value="editor" className="flex-1">Editor</TabsTrigger>
<TabsTrigger value="ai" className="flex-1">AI Assistant</TabsTrigger> <TabsTrigger value="ai" className="flex-1">AI</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="editor" className="flex-1 m-0"> <TabsContent value="files" className="flex-1 m-0">
<CodeEditor onCodeChange={setCurrentCode} /> <FileTree
files={files || []}
onFileSelect={handleFileSelect}
onFileCreate={handleFileCreate}
onFileRename={handleFileRename}
onFileDelete={handleFileDelete}
selectedFileId={activeFileId}
/>
</TabsContent>
<TabsContent value="editor" className="flex-1 m-0 flex flex-col">
<FileTabs
openFiles={openFiles || []}
activeFileId={activeFileId}
onFileSelect={handleFileSelect}
onFileClose={handleFileClose}
/>
<div className="flex-1">
<CodeEditor onCodeChange={setCurrentCode} />
</div>
</TabsContent> </TabsContent>
<TabsContent value="ai" className="flex-1 m-0"> <TabsContent value="ai" className="flex-1 m-0">
<AIChat currentCode={currentCode} /> <AIChat currentCode={currentCode} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
) : ( ) : (
<ResizablePanelGroup direction="horizontal"> <>
<ResizablePanel defaultSize={60} minSize={30}> <div className="flex-1 overflow-hidden">
<CodeEditor onCodeChange={setCurrentCode} /> <ResizablePanelGroup direction="horizontal">
</ResizablePanel> <ResizablePanel defaultSize={15} minSize={10} maxSize={25}>
<FileTree
files={files || []}
onFileSelect={handleFileSelect}
onFileCreate={handleFileCreate}
onFileRename={handleFileRename}
onFileDelete={handleFileDelete}
selectedFileId={activeFileId}
/>
</ResizablePanel>
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" /> <ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
<ResizablePanel defaultSize={40} minSize={25}> <ResizablePanel defaultSize={55} minSize={30}>
<AIChat currentCode={currentCode} /> <div className="h-full flex flex-col">
</ResizablePanel> <FileTabs
</ResizablePanelGroup> openFiles={openFiles || []}
activeFileId={activeFileId}
onFileSelect={handleFileSelect}
onFileClose={handleFileClose}
/>
<div className="flex-1">
<CodeEditor onCodeChange={setCurrentCode} />
</div>
</div>
</ResizablePanel>
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
<ResizablePanel defaultSize={30} minSize={20}>
<AIChat currentCode={currentCode} />
</ResizablePanel>
</ResizablePanelGroup>
</div>
<ConsolePanel />
</>
)} )}
</div> </div>
@ -61,6 +269,18 @@ function App() {
/> />
)} )}
<PreviewModal
open={showPreview}
onClose={() => setShowPreview(false)}
code={currentCode}
/>
<NewProjectModal
open={showNewProject}
onClose={() => setShowNewProject(false)}
onCreateProject={handleCreateProject}
/>
<WelcomeDialog /> <WelcomeDialog />
<Toaster position="bottom-right" theme="dark" /> <Toaster position="bottom-right" theme="dark" />
</div> </div>

View file

@ -0,0 +1,193 @@
import { useState, useEffect, useRef } from 'react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Trash, Terminal } from '@phosphor-icons/react';
interface ConsoleLog {
id: string;
timestamp: Date;
type: 'log' | 'warn' | 'error' | 'info';
platform: 'roblox' | 'web' | 'mobile' | 'system';
message: string;
}
interface ConsolePanelProps {
collapsed?: boolean;
onToggle?: () => void;
}
export function ConsolePanel({ collapsed, onToggle }: ConsolePanelProps) {
const [logs, setLogs] = useState<ConsoleLog[]>([
{
id: '1',
timestamp: new Date(),
type: 'info',
platform: 'system',
message: 'AeThex Studio Console initialized',
},
{
id: '2',
timestamp: new Date(),
type: 'log',
platform: 'roblox',
message: 'Player joined the game!',
},
]);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [logs]);
const clearLogs = () => {
setLogs([
{
id: Date.now().toString(),
timestamp: new Date(),
type: 'info',
platform: 'system',
message: 'Console cleared',
},
]);
};
const getLogColor = (type: ConsoleLog['type']) => {
switch (type) {
case 'error':
return 'text-red-400';
case 'warn':
return 'text-yellow-400';
case 'info':
return 'text-blue-400';
default:
return 'text-foreground';
}
};
const getPlatformBadgeColor = (platform: ConsoleLog['platform']) => {
switch (platform) {
case 'roblox':
return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
case 'web':
return 'bg-green-500/20 text-green-400 border-green-500/30';
case 'mobile':
return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
default:
return 'bg-muted/50 text-muted-foreground';
}
};
if (collapsed) {
return (
<div className="h-8 bg-card border-t border-border flex items-center justify-between px-4 cursor-pointer" onClick={onToggle}>
<div className="flex items-center gap-2">
<Terminal size={16} />
<span className="text-sm font-semibold">Console</span>
<Badge variant="secondary" className="text-xs">
{logs.length}
</Badge>
</div>
</div>
);
}
return (
<div className="h-[200px] bg-card border-t border-border flex flex-col">
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
<div className="flex items-center gap-2">
<Terminal size={18} />
<span className="text-sm font-semibold">Console</span>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={clearLogs}>
<Trash size={16} />
</Button>
</div>
</div>
<Tabs defaultValue="all" className="flex-1 flex flex-col">
<TabsList className="mx-4 mt-2 h-8 w-fit">
<TabsTrigger value="all" className="text-xs">All</TabsTrigger>
<TabsTrigger value="roblox" className="text-xs">Roblox</TabsTrigger>
<TabsTrigger value="web" className="text-xs">Web</TabsTrigger>
<TabsTrigger value="mobile" className="text-xs">Mobile</TabsTrigger>
</TabsList>
<TabsContent value="all" className="flex-1 m-0">
<ScrollArea className="h-[140px]" ref={scrollRef}>
<div className="px-4 py-2 space-y-1 font-mono text-xs">
{logs.map((log) => (
<div key={log.id} className="flex items-start gap-2 py-1">
<span className="text-muted-foreground flex-shrink-0">
{log.timestamp.toLocaleTimeString()}
</span>
<Badge variant="outline" className={`flex-shrink-0 text-xs ${getPlatformBadgeColor(log.platform)}`}>
{log.platform}
</Badge>
<span className={`${getLogColor(log.type)} flex-1`}>
{log.message}
</span>
</div>
))}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="roblox" className="flex-1 m-0">
<ScrollArea className="h-[140px]">
<div className="px-4 py-2 space-y-1 font-mono text-xs">
{logs.filter((log) => log.platform === 'roblox').map((log) => (
<div key={log.id} className="flex items-start gap-2 py-1">
<span className="text-muted-foreground flex-shrink-0">
{log.timestamp.toLocaleTimeString()}
</span>
<span className={getLogColor(log.type)}>
{log.message}
</span>
</div>
))}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="web" className="flex-1 m-0">
<ScrollArea className="h-[140px]">
<div className="px-4 py-2 space-y-1 font-mono text-xs">
{logs.filter((log) => log.platform === 'web').map((log) => (
<div key={log.id} className="flex items-start gap-2 py-1">
<span className="text-muted-foreground flex-shrink-0">
{log.timestamp.toLocaleTimeString()}
</span>
<span className={getLogColor(log.type)}>
{log.message}
</span>
</div>
))}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="mobile" className="flex-1 m-0">
<ScrollArea className="h-[140px]">
<div className="px-4 py-2 space-y-1 font-mono text-xs">
{logs.filter((log) => log.platform === 'mobile').map((log) => (
<div key={log.id} className="flex items-start gap-2 py-1">
<span className="text-muted-foreground flex-shrink-0">
{log.timestamp.toLocaleTimeString()}
</span>
<span className={getLogColor(log.type)}>
{log.message}
</span>
</div>
))}
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</div>
);
}

View file

@ -0,0 +1,51 @@
import { X } from '@phosphor-icons/react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { FileNode } from './FileTree';
interface FileTabsProps {
openFiles: FileNode[];
activeFileId?: string;
onFileSelect: (file: FileNode) => void;
onFileClose: (id: string) => void;
}
export function FileTabs({
openFiles,
activeFileId,
onFileSelect,
onFileClose,
}: FileTabsProps) {
if (openFiles.length === 0) return null;
return (
<div className="flex items-center bg-card/50 border-b border-border">
<div className="flex overflow-x-auto flex-1">
{openFiles.map((file) => (
<div
key={file.id}
className={`flex items-center gap-2 px-4 py-2 border-r border-border cursor-pointer hover:bg-muted/50 transition-colors group min-w-0 ${
activeFileId === file.id
? 'bg-background border-b-2 border-b-accent'
: ''
}`}
onClick={() => onFileSelect(file)}
>
<span className="text-sm truncate max-w-[120px]">{file.name}</span>
<Button
variant="ghost"
size="icon"
className="h-4 w-4 p-0 opacity-0 group-hover:opacity-100 hover:text-destructive flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
onFileClose(file.id);
}}
>
<X size={14} />
</Button>
</div>
))}
</div>
</div>
);
}

187
src/components/FileTree.tsx Normal file
View file

@ -0,0 +1,187 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Input } from '@/components/ui/input';
import {
File,
Folder,
FolderOpen,
Plus,
DotsThree,
Trash,
PencilSimple,
} from '@phosphor-icons/react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { toast } from 'sonner';
export interface FileNode {
id: string;
name: string;
type: 'file' | 'folder';
children?: FileNode[];
content?: string;
}
interface FileTreeProps {
files: FileNode[];
onFileSelect: (file: FileNode) => void;
onFileCreate: (name: string, parentId?: string) => void;
onFileRename: (id: string, newName: string) => void;
onFileDelete: (id: string) => void;
selectedFileId?: string;
}
export function FileTree({
files,
onFileSelect,
onFileCreate,
onFileRename,
onFileDelete,
selectedFileId,
}: FileTreeProps) {
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(['root']));
const [editingId, setEditingId] = useState<string | null>(null);
const [editingName, setEditingName] = useState('');
const toggleFolder = (id: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const startRename = (file: FileNode) => {
setEditingId(file.id);
setEditingName(file.name);
};
const finishRename = (id: string) => {
if (editingName.trim() && editingName !== '') {
onFileRename(id, editingName.trim());
toast.success('File renamed');
}
setEditingId(null);
setEditingName('');
};
const handleDelete = (file: FileNode) => {
if (confirm(`Delete ${file.name}?`)) {
onFileDelete(file.id);
toast.success('File deleted');
}
};
const renderNode = (node: FileNode, depth: number = 0) => {
const isExpanded = expandedFolders.has(node.id);
const isSelected = selectedFileId === node.id;
const isEditing = editingId === node.id;
return (
<div key={node.id}>
<div
className={`flex items-center gap-2 px-2 py-1.5 hover:bg-muted/50 cursor-pointer group rounded ${
isSelected ? 'bg-accent/20 text-accent' : ''
}`}
style={{ paddingLeft: `${depth * 12 + 8}px` }}
onClick={() => {
if (node.type === 'folder') {
toggleFolder(node.id);
} else {
onFileSelect(node);
}
}}
>
{node.type === 'folder' ? (
isExpanded ? (
<FolderOpen size={16} className="flex-shrink-0" />
) : (
<Folder size={16} className="flex-shrink-0" />
)
) : (
<File size={16} className="flex-shrink-0" />
)}
{isEditing ? (
<Input
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
onBlur={() => finishRename(node.id)}
onKeyDown={(e) => {
if (e.key === 'Enter') finishRename(node.id);
if (e.key === 'Escape') setEditingId(null);
}}
className="h-6 text-sm"
autoFocus
onClick={(e) => e.stopPropagation()}
/>
) : (
<span className="text-sm flex-1 truncate">{node.name}</span>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 opacity-0 group-hover:opacity-100"
>
<DotsThree />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => startRename(node)}>
<PencilSimple className="mr-2" />
Rename
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDelete(node)}
className="text-destructive"
>
<Trash className="mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{node.type === 'folder' && isExpanded && node.children && (
<div>{node.children.map((child) => renderNode(child, depth + 1))}</div>
)}
</div>
);
};
return (
<div className="flex flex-col h-full bg-card border-r border-border">
<div className="flex items-center justify-between p-3 border-b border-border">
<span className="text-sm font-semibold">Explorer</span>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => {
const name = prompt('Enter file name:');
if (name) {
onFileCreate(name);
}
}}
>
<Plus size={16} />
</Button>
</div>
<ScrollArea className="flex-1">
<div className="p-2">{files.map((node) => renderNode(node))}</div>
</ScrollArea>
</div>
);
}

View file

@ -0,0 +1,370 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Switch } from '@/components/ui/switch';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { GameController, Globe, DeviceMobile, FileCode, Info, Check } from '@phosphor-icons/react';
import { toast } from 'sonner';
interface NewProjectModalProps {
open: boolean;
onClose: () => void;
onCreateProject: (config: ProjectConfig) => void;
}
export interface ProjectConfig {
name: string;
template: string;
platforms: {
roblox: boolean;
web: boolean;
mobile: boolean;
};
nexusEngine: boolean;
passportAuth: boolean;
}
const templates = [
{
id: 'roblox-starter',
name: 'Roblox Game Starter',
description: 'Basic Roblox game template with player management and leaderboards',
icon: '🎮',
popular: true,
},
{
id: 'multiplayer',
name: 'Cross-Platform Multiplayer',
description: 'Synchronized multiplayer game across Roblox, web, and mobile',
icon: '🌐',
popular: true,
},
{
id: 'transmedia',
name: 'Transmedia Story Project',
description: 'Story-driven experience that spans multiple platforms',
icon: '📚',
popular: false,
},
{
id: 'blank',
name: 'Blank Project',
description: 'Start from scratch with an empty project',
icon: '📄',
popular: false,
},
];
export function NewProjectModal({ open, onClose, onCreateProject }: NewProjectModalProps) {
const [step, setStep] = useState(1);
const [projectName, setProjectName] = useState('');
const [selectedTemplate, setSelectedTemplate] = useState('');
const [platforms, setPlatforms] = useState({
roblox: true,
web: false,
mobile: false,
});
const [nexusEngine, setNexusEngine] = useState(false);
const [passportAuth, setPassportAuth] = useState(false);
const handleReset = () => {
setStep(1);
setProjectName('');
setSelectedTemplate('');
setPlatforms({ roblox: true, web: false, mobile: false });
setNexusEngine(false);
setPassportAuth(false);
};
const handleCreate = () => {
if (!projectName.trim()) {
toast.error('Please enter a project name');
return;
}
if (!selectedTemplate) {
toast.error('Please select a template');
return;
}
onCreateProject({
name: projectName,
template: selectedTemplate,
platforms,
nexusEngine,
passportAuth,
});
handleReset();
onClose();
toast.success(`Project "${projectName}" created!`);
};
const handleClose = () => {
handleReset();
onClose();
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle className="text-2xl">Create New Project</DialogTitle>
</DialogHeader>
<div className="flex items-center justify-center gap-2 mb-6">
{[1, 2, 3].map((s) => (
<div key={s} className="flex items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold ${
s <= step
? 'bg-accent text-accent-foreground'
: 'bg-muted text-muted-foreground'
}`}
>
{s < step ? <Check size={16} weight="bold" /> : s}
</div>
{s < 3 && (
<div
className={`w-16 h-1 ${
s < step ? 'bg-accent' : 'bg-muted'
}`}
/>
)}
</div>
))}
</div>
{step === 1 && (
<div>
<h3 className="text-lg font-semibold mb-4">Choose a Template</h3>
<div className="grid grid-cols-2 gap-4 max-h-[400px] overflow-y-auto">
{templates.map((template) => (
<Card
key={template.id}
className={`p-4 cursor-pointer transition-all hover:border-accent ${
selectedTemplate === template.id
? 'border-2 border-accent bg-accent/5'
: ''
}`}
onClick={() => setSelectedTemplate(template.id)}
>
<div className="flex items-start justify-between mb-2">
<div className="text-3xl">{template.icon}</div>
{template.popular && (
<Badge variant="secondary" className="text-xs">
Popular
</Badge>
)}
</div>
<h4 className="font-semibold mb-1">{template.name}</h4>
<p className="text-sm text-muted-foreground">
{template.description}
</p>
</Card>
))}
</div>
<div className="flex justify-end gap-2 mt-6">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button
onClick={() => setStep(2)}
disabled={!selectedTemplate}
className="bg-accent text-accent-foreground hover:bg-accent/90"
>
Next
</Button>
</div>
</div>
)}
{step === 2 && (
<div>
<h3 className="text-lg font-semibold mb-4">Project Configuration</h3>
<div className="space-y-6">
<div>
<Label htmlFor="project-name">Project Name</Label>
<Input
id="project-name"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
placeholder="My Awesome Game"
className="mt-2"
/>
</div>
<div>
<Label className="mb-3 block">Target Platforms</Label>
<div className="space-y-3">
<div className="flex items-center gap-3">
<Checkbox
id="platform-roblox"
checked={platforms.roblox}
onCheckedChange={(checked) =>
setPlatforms((p) => ({ ...p, roblox: checked as boolean }))
}
/>
<Label htmlFor="platform-roblox" className="flex items-center gap-2 cursor-pointer">
<GameController className="text-accent" />
Roblox
</Label>
</div>
<div className="flex items-center gap-3">
<Checkbox
id="platform-web"
checked={platforms.web}
onCheckedChange={(checked) =>
setPlatforms((p) => ({ ...p, web: checked as boolean }))
}
/>
<Label htmlFor="platform-web" className="flex items-center gap-2 cursor-pointer">
<Globe className="text-accent" />
Web Browser
</Label>
</div>
<div className="flex items-center gap-3">
<Checkbox
id="platform-mobile"
checked={platforms.mobile}
onCheckedChange={(checked) =>
setPlatforms((p) => ({ ...p, mobile: checked as boolean }))
}
/>
<Label htmlFor="platform-mobile" className="flex items-center gap-2 cursor-pointer">
<DeviceMobile className="text-accent" />
Mobile (iOS/Android)
</Label>
</div>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Label htmlFor="nexus-engine">Enable Nexus Engine</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Info size={16} className="text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs text-sm">
Nexus Engine synchronizes game state across all platforms in real-time
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Switch
id="nexus-engine"
checked={nexusEngine}
onCheckedChange={setNexusEngine}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Label htmlFor="passport-auth">Enable Passport Auth</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Info size={16} className="text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs text-sm">
AeThex Passport provides unified identity across all your games
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Switch
id="passport-auth"
checked={passportAuth}
onCheckedChange={setPassportAuth}
/>
</div>
</div>
</div>
<div className="flex justify-between mt-6">
<Button variant="outline" onClick={() => setStep(1)}>
Back
</Button>
<div className="flex gap-2">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button
onClick={() => setStep(3)}
className="bg-accent text-accent-foreground hover:bg-accent/90"
>
Next
</Button>
</div>
</div>
</div>
)}
{step === 3 && (
<div>
<h3 className="text-lg font-semibold mb-4">Review & Create</h3>
<Card className="p-4 space-y-3">
<div>
<Label className="text-muted-foreground">Project Name</Label>
<p className="font-semibold">{projectName}</p>
</div>
<div>
<Label className="text-muted-foreground">Template</Label>
<p className="font-semibold">
{templates.find((t) => t.id === selectedTemplate)?.name}
</p>
</div>
<div>
<Label className="text-muted-foreground">Platforms</Label>
<div className="flex gap-2 mt-1">
{platforms.roblox && <Badge>Roblox</Badge>}
{platforms.web && <Badge>Web</Badge>}
{platforms.mobile && <Badge>Mobile</Badge>}
</div>
</div>
<div>
<Label className="text-muted-foreground">Features</Label>
<div className="flex gap-2 mt-1">
{nexusEngine && <Badge variant="secondary">Nexus Engine</Badge>}
{passportAuth && <Badge variant="secondary">Passport Auth</Badge>}
{!nexusEngine && !passportAuth && (
<span className="text-sm text-muted-foreground">None</span>
)}
</div>
</div>
</Card>
<div className="flex justify-between mt-6">
<Button variant="outline" onClick={() => setStep(2)}>
Back
</Button>
<div className="flex gap-2">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button
onClick={handleCreate}
className="bg-accent text-accent-foreground hover:bg-accent/90"
>
<FileCode className="mr-2" />
Create Project
</Button>
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,207 @@
import { useState } from 'react';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { X, ArrowsClockwise } from '@phosphor-icons/react';
interface PreviewModalProps {
open: boolean;
onClose: () => void;
code: string;
}
interface SharedState {
variable: string;
roblox: string;
web: string;
mobile: string;
status: 'synced' | 'syncing' | 'conflict';
}
export function PreviewModal({ open, onClose, code }: PreviewModalProps) {
const [sharedState] = useState<SharedState[]>([
{ variable: 'playerX', roblox: '120', web: '120', mobile: '120', status: 'synced' },
{ variable: 'playerY', roblox: '50', web: '50', mobile: '50', status: 'synced' },
{ variable: 'health', roblox: '100', web: '98', mobile: '100', status: 'syncing' },
{ variable: 'score', roblox: '450', web: '450', mobile: '450', status: 'synced' },
]);
const getStatusColor = (status: SharedState['status']) => {
switch (status) {
case 'synced':
return 'text-green-500';
case 'syncing':
return 'text-yellow-500';
case 'conflict':
return 'text-red-500';
}
};
const getStatusIcon = (status: SharedState['status']) => {
switch (status) {
case 'synced':
return '✓';
case 'syncing':
return '⚠';
case 'conflict':
return '✗';
}
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-[95vw] h-[90vh] p-0 gap-0">
<div className="flex flex-col h-full">
<div className="flex items-center justify-between p-4 border-b border-border">
<h2 className="text-xl font-bold">Preview - All Platforms</h2>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<ArrowsClockwise className="mr-2" />
Refresh All
</Button>
<Button variant="ghost" size="icon" onClick={onClose}>
<X />
</Button>
</div>
</div>
<Tabs defaultValue="all" className="flex-1 flex flex-col">
<TabsList className="mx-4 mt-4 w-fit">
<TabsTrigger value="all">All Platforms</TabsTrigger>
<TabsTrigger value="roblox">Roblox</TabsTrigger>
<TabsTrigger value="web">Web</TabsTrigger>
<TabsTrigger value="mobile">Mobile</TabsTrigger>
</TabsList>
<TabsContent value="all" className="flex-1 flex flex-col p-4 gap-4 m-0">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 flex-1">
<Card className="p-4 flex flex-col">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold">Roblox</h3>
<Button size="sm" variant="outline">Deploy</Button>
</div>
<div className="flex-1 bg-muted/30 rounded flex items-center justify-center border-2 border-dashed border-border">
<div className="text-center p-8">
<div className="text-6xl mb-4">🎮</div>
<p className="text-sm text-muted-foreground">Roblox Viewport</p>
<p className="text-xs text-muted-foreground mt-1">Studio integration</p>
</div>
</div>
</Card>
<Card className="p-4 flex flex-col">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold">Web Browser</h3>
<Button size="sm" variant="outline">Deploy</Button>
</div>
<div className="flex-1 bg-muted/30 rounded flex items-center justify-center border-2 border-dashed border-border">
<div className="text-center p-8">
<div className="text-6xl mb-4">🌐</div>
<p className="text-sm text-muted-foreground">Web Canvas</p>
<p className="text-xs text-muted-foreground mt-1">Browser preview</p>
</div>
</div>
</Card>
<Card className="p-4 flex flex-col">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold">Mobile</h3>
<Button size="sm" variant="outline">Deploy</Button>
</div>
<div className="flex-1 bg-muted/30 rounded flex items-center justify-center border-2 border-dashed border-border">
<div className="w-[200px] h-full bg-background rounded-3xl border-4 border-foreground/20 p-2 flex flex-col">
<div className="w-1/3 h-6 bg-foreground/10 rounded-full mx-auto mb-2" />
<div className="flex-1 bg-muted/30 rounded-2xl flex items-center justify-center">
<div className="text-center">
<div className="text-4xl mb-2">📱</div>
<p className="text-xs text-muted-foreground">Mobile View</p>
</div>
</div>
<div className="h-6 w-1/2 bg-foreground/10 rounded-full mx-auto mt-2" />
</div>
</div>
</Card>
</div>
<Card className="p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold">Shared State Sync</h3>
<Badge variant="outline" className="text-green-500 border-green-500">
Live Sync Enabled
</Badge>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="border-b border-border">
<tr className="text-left">
<th className="pb-2 font-semibold">Variable</th>
<th className="pb-2 font-semibold">Roblox</th>
<th className="pb-2 font-semibold">Web</th>
<th className="pb-2 font-semibold">Mobile</th>
<th className="pb-2 font-semibold">Status</th>
</tr>
</thead>
<tbody>
{sharedState.map((state, idx) => (
<tr key={idx} className="border-b border-border/50">
<td className="py-2 font-mono text-accent">{state.variable}</td>
<td className="py-2 font-mono">{state.roblox}</td>
<td className="py-2 font-mono">{state.web}</td>
<td className="py-2 font-mono">{state.mobile}</td>
<td className="py-2">
<span className={getStatusColor(state.status)}>
{getStatusIcon(state.status)} {state.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</TabsContent>
<TabsContent value="roblox" className="flex-1 p-4 m-0">
<Card className="h-full p-8 flex items-center justify-center">
<div className="text-center">
<div className="text-8xl mb-4">🎮</div>
<h3 className="text-2xl font-bold mb-2">Roblox Preview</h3>
<p className="text-muted-foreground mb-4">Full-screen Roblox viewport</p>
<Button>Deploy to Roblox</Button>
</div>
</Card>
</TabsContent>
<TabsContent value="web" className="flex-1 p-4 m-0">
<Card className="h-full p-8 flex items-center justify-center">
<div className="text-center">
<div className="text-8xl mb-4">🌐</div>
<h3 className="text-2xl font-bold mb-2">Web Preview</h3>
<p className="text-muted-foreground mb-4">Browser-based game preview</p>
<Button>Deploy to Web</Button>
</div>
</Card>
</TabsContent>
<TabsContent value="mobile" className="flex-1 p-4 m-0 flex items-center justify-center">
<div className="w-[300px] h-[600px] bg-background rounded-3xl border-8 border-foreground/20 p-4 flex flex-col shadow-2xl">
<div className="w-1/2 h-8 bg-foreground/10 rounded-full mx-auto mb-4" />
<Card className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4">📱</div>
<h3 className="text-xl font-bold mb-2">Mobile Preview</h3>
<p className="text-sm text-muted-foreground mb-4">iOS/Android view</p>
<Button size="sm">Deploy to Mobile</Button>
</div>
</Card>
<div className="h-8 w-1/2 bg-foreground/10 rounded-full mx-auto mt-4" />
</div>
</TabsContent>
</Tabs>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -1,17 +1,32 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Copy, FileCode, Download, Info } from '@phosphor-icons/react'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Copy, FileCode, Download, Info, Play, FolderPlus, User, SignOut } from '@phosphor-icons/react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
interface ToolbarProps { interface ToolbarProps {
code: string; code: string;
onTemplatesClick: () => void; onTemplatesClick: () => void;
onPreviewClick: () => void;
onNewProjectClick: () => void;
} }
export function Toolbar({ code, onTemplatesClick }: ToolbarProps) { export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectClick }: ToolbarProps) {
const [showInfo, setShowInfo] = useState(false); const [showInfo, setShowInfo] = useState(false);
const [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(null);
useEffect(() => {
window.spark.user().then(setUser).catch(() => setUser(null));
}, []);
const handleCopy = async () => { const handleCopy = async () => {
try { try {
@ -41,10 +56,40 @@ export function Toolbar({ code, onTemplatesClick }: ToolbarProps) {
<h1 className="text-2xl font-bold tracking-tight"> <h1 className="text-2xl font-bold tracking-tight">
Ae<span className="text-accent">Thex</span> Ae<span className="text-accent">Thex</span>
</h1> </h1>
<span className="text-sm text-muted-foreground ml-1">Studio</span>
<div className="flex-1" /> <div className="flex-1" />
<TooltipProvider> <TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
size="sm"
onClick={onNewProjectClick}
className="bg-accent text-accent-foreground hover:bg-accent/90"
>
<FolderPlus />
<span className="ml-2 hidden sm:inline">New Project</span>
</Button>
</TooltipTrigger>
<TooltipContent>Create a new project</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={onPreviewClick}
>
<Play />
<span className="ml-2 hidden sm:inline">Preview</span>
</Button>
</TooltipTrigger>
<TooltipContent>Preview all platforms</TooltipContent>
</Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="ghost" size="sm" onClick={onTemplatesClick}> <Button variant="ghost" size="sm" onClick={onTemplatesClick}>
@ -89,6 +134,38 @@ export function Toolbar({ code, onTemplatesClick }: ToolbarProps) {
<TooltipContent>About AeThex Studio</TooltipContent> <TooltipContent>About AeThex Studio</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src={user?.avatarUrl} alt={user?.login || 'User'} />
<AvatarFallback>
<User />
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{user && (
<>
<div className="px-2 py-1.5">
<p className="text-sm font-semibold">{user.login}</p>
<p className="text-xs text-muted-foreground">{user.email}</p>
</div>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={() => toast.info('Profile coming soon!')}>
<User className="mr-2" />
Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toast.info('Sign out feature coming soon!')}>
<SignOut className="mr-2" />
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
<Dialog open={showInfo} onOpenChange={setShowInfo}> <Dialog open={showInfo} onOpenChange={setShowInfo}>