diff --git a/PRD.md b/PRD.md index 3d4acbb..91df01a 100644 --- a/PRD.md +++ b/PRD.md @@ -1,61 +1,70 @@ # 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**: -1. **Professional** - Should feel like a legitimate development tool with clean typography and purposeful spacing -2. **Intelligent** - AI assistance should feel seamlessly integrated, not bolted on -3. **Empowering** - Users should feel confident writing and understanding Lua code +1. **Professional** - Should feel like a complete IDE with file navigation, tabs, and split panels like VS Code +2. **Intelligent** - AI assistance deeply integrated with context awareness and code suggestions +3. **Cross-Platform** - Unified workspace showing Roblox, Web, and Mobile development simultaneously -**Complexity Level**: Light Application (multiple features with basic state) -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. +**Complexity Level**: Complex Application (advanced functionality with multiple views and state management) +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 -### Monaco Code Editor -- **Functionality**: Full-featured Lua code editor with syntax highlighting, autocomplete, and line numbers -- **Purpose**: Provide a professional coding environment for Roblox scripting -- **Trigger**: Loads on app start with default template -- **Progression**: User opens app → sees editor with starter code → begins typing → receives syntax highlighting and autocomplete -- **Success criteria**: Code is editable, syntax-highlighted, and changes persist between sessions +### File Tree Navigator +- **Functionality**: Left sidebar showing folder/file hierarchy with create, rename, and delete operations +- **Purpose**: Organize multiple scripts and manage project structure like a real IDE +- **Trigger**: Always visible on desktop, accessible via tabs on mobile +- **Progression**: User clicks "New File" → enters name → new file appears in tree → clicks to open +- **Success criteria**: Files persist between sessions, operations work smoothly, selected file highlights -### AI Chat Assistant -- **Functionality**: Conversational AI panel that helps explain code, debug issues, and suggest improvements -- **Purpose**: Lower the learning curve for new Roblox developers -- **Trigger**: User clicks chat button or types a question in the AI panel -- **Progression**: User asks question → AI analyzes current code → provides contextual answer with code examples -- **Success criteria**: AI responses are relevant, helpful, and include code snippets when appropriate +### Multi-File Tabs +- **Functionality**: Horizontal tabs showing open files with close buttons +- **Purpose**: Work with multiple scripts simultaneously without losing context +- **Trigger**: Opening a file from tree adds a tab +- **Progression**: User opens file → tab appears → clicks tab to switch → clicks X to close +- **Success criteria**: Active tab highlighted, tab state persists, smooth switching -### Script Templates -- **Functionality**: Pre-built Roblox script templates (basic part manipulation, player join events, GUI interactions, etc.) -- **Purpose**: Jump-start development and teach common patterns -- **Trigger**: User clicks "Templates" button -- **Progression**: User browses templates → clicks template → code loads into editor → user modifies for their needs -- **Success criteria**: Templates load instantly and represent common Roblox use cases +### Multi-Platform Preview +- **Functionality**: Modal showing Roblox, Web, and Mobile viewports side-by-side with shared state sync +- **Purpose**: Visualize how game runs across all platforms simultaneously +- **Trigger**: User clicks "Preview" button in toolbar +- **Progression**: Click Preview → modal opens → see three viewport mockups → view synced state table → close modal +- **Success criteria**: Modal is visually impressive with glassmorphism, state sync table updates, deploy buttons present -### Code Validation -- **Functionality**: Real-time syntax checking and error highlighting -- **Purpose**: Catch errors before testing in Roblox Studio -- **Trigger**: Automatic as user types -- **Progression**: User types invalid Lua → error highlights appear → user hovers for details → fixes error -- **Success criteria**: Common Lua syntax errors are caught and clearly displayed +### New Project Wizard +- **Functionality**: 3-step modal for creating projects with templates, platform selection, and feature toggles +- **Purpose**: Quick-start new games with appropriate scaffolding +- **Trigger**: User clicks "New Project" button in toolbar +- **Progression**: Click New Project → choose template → configure settings → review → create → file tree populates +- **Success criteria**: Stepper UI shows progress, all template types available, settings persist -### Export/Copy Code -- **Functionality**: One-click copy of current code to clipboard -- **Purpose**: Easy transfer to Roblox Studio -- **Trigger**: User clicks copy button -- **Progression**: User finishes editing → clicks copy → receives confirmation → pastes into Roblox Studio -- **Success criteria**: Code copies to clipboard with proper formatting +### Console Output Panel +- **Functionality**: Bottom panel showing logs filtered by platform (Roblox/Web/Mobile/System) +- **Purpose**: Display runtime output and errors from all platforms in one place +- **Trigger**: Always visible on desktop (collapsible), shows logs automatically +- **Progression**: Code runs → logs appear → filter by platform → clear logs button +- **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 -- **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 -- **Large Scripts**: Monaco handles large files, but warn if script exceeds 5000 lines (unusual for Roblox scripts) -- **Invalid Lua**: Show errors inline without blocking the user from continuing to type -- **Lost Work**: Auto-save code every 30 seconds to prevent data loss +- **Large Scripts**: Monaco handles large files, warn if exceeds 10,000 lines +- **Invalid Operations**: Prevent deleting root folder, renaming to empty string +- **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 -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 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 - **Components**: - - `Button` (primary actions like "Run", "Copy", "Ask AI") with custom variants for destructive and accent states - - `Card` for template previews and code snippets - - `Separator` for dividing editor from chat panel - - `ScrollArea` for chat messages and template list - - `Tabs` for switching between templates and settings - - `Dialog` for first-time user onboarding - - `Badge` for Lua syntax error indicators - - `Tooltip` for icon button explanations - - Custom Monaco Editor wrapper component - - Custom AI chat component with message bubbles + - `Button` with accent variant for primary actions (New Project, Preview) + - `Card` for template previews, project configs, and preview mockups + - `Dialog` for Preview Modal, New Project Modal, About dialog + - `DropdownMenu` for file context menus and user profile menu + - `Avatar` for user profile display + - `ScrollArea` for file tree, chat messages, console logs + - `Tabs` for template categories, preview modes, console filters, mobile navigation + - `Input` for file rename, project name entry + - `Checkbox` for platform selection in new project + - `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**: - - Monaco editor needs dark theme configuration - - Chat bubbles with distinct styling for user vs AI messages - - Template cards with code preview using syntax-highlighted pre blocks - - Resizable panels using react-resizable-panels + - Monaco editor with dark theme configuration + - File tree with expand/collapse animations + - Preview modal with glassmorphism effects (backdrop-blur) + - Multi-platform viewport mockups with device frames + - Shared state sync table with color-coded status + - Console with platform-specific badge colors - **States**: - - Buttons: Default (solid accent), Hover (brightened +10% lightness), Active (scale 0.97), Disabled (50% opacity) - - Editor: Default (dark background), Focused (subtle glow border) - - Chat input: Default (muted border), Focused (accent border with glow) + - Buttons: Accent primary (New Project), Ghost secondary (Templates, Copy) + - File items: Selected (accent background), Hover (muted background) + - Tabs: Active (accent bottom border), Inactive (transparent) + - Console logs: Color by type (red error, yellow warn, blue info) - **Icon Selection**: - - Code (editor): `` from Phosphor - - Chat: `` from Phosphor - - Copy: `` from Phosphor - - Templates: `` from Phosphor - - AI sparkle: `` from Phosphor - - Play/Run: `` from Phosphor + - File tree: ``, ``, ``, ``, `` + - Toolbar: `` (New Project), `` (Preview), `` (Profile) + - Preview: `` (Refresh), ``, ``, `` + - Console: `` (Console icon) + - All from Phosphor Icons - **Spacing**: - - Container padding: p-4 (16px) - - Component gaps: gap-4 (16px) for major sections, gap-2 (8px) for grouped items - - Button padding: px-6 py-2.5 - - Card padding: p-6 + - Container padding: p-4 + - Panel borders: border-border (1px) + - Component gaps: gap-4 for sections, gap-2 for grouped items + - File tree indent: 12px per level - **Mobile**: - - Stack editor and chat vertically on mobile - - Make chat panel collapsible on small screens - - Reduce font sizes: H1 to 24px, body to 13px - - Full-width buttons with bottom sheet for templates - - Hide Monaco minimap on screens < 768px + - Tabs for Files/Editor/AI switching on mobile + - File tree full-screen on mobile + - Hide console panel on mobile + - Preview modal scrollable on small screens + - Stepper in New Project modal scales down diff --git a/src/App.tsx b/src/App.tsx index 0831574..0457f36 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,52 +5,260 @@ import { AIChat } from '@/components/AIChat'; import { Toolbar } from '@/components/Toolbar'; import { TemplatesDrawer } from '@/components/TemplatesDrawer'; 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 { useKV } from '@github/spark/hooks'; import { useIsMobile } from '@/hooks/use-mobile'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { toast } from 'sonner'; function App() { const [currentCode, setCurrentCode] = useState(''); const [showTemplates, setShowTemplates] = useState(false); + const [showPreview, setShowPreview] = useState(false); + const [showNewProject, setShowNewProject] = useState(false); const [code, setCode] = useKV('aethex-current-code', ''); const isMobile = useIsMobile(); + const [files, setFiles] = useKV('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('aethex-open-files', []); + const [activeFileId, setActiveFileId] = useKV('aethex-active-file', 'file-1'); + const handleTemplateSelect = (templateCode: string) => { setCode(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 (
- setShowTemplates(true)} /> + setShowTemplates(true)} + onPreviewClick={() => setShowPreview(true)} + onNewProjectClick={() => setShowNewProject(true)} + /> -
+
{isMobile ? ( + Files Editor - AI Assistant + AI - - + + + + + +
+ +
) : ( - - - - + <> +
+ + + + - + - - - - + +
+ +
+ +
+
+
+ + + + + + + +
+ + )}
@@ -61,6 +269,18 @@ function App() { /> )} + setShowPreview(false)} + code={currentCode} + /> + + setShowNewProject(false)} + onCreateProject={handleCreateProject} + /> +
diff --git a/src/components/ConsolePanel.tsx b/src/components/ConsolePanel.tsx new file mode 100644 index 0000000..1d485b7 --- /dev/null +++ b/src/components/ConsolePanel.tsx @@ -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([ + { + 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(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 ( +
+
+ + Console + + {logs.length} + +
+
+ ); + } + + return ( +
+
+
+ + Console +
+
+ +
+
+ + + + All + Roblox + Web + Mobile + + + + +
+ {logs.map((log) => ( +
+ + {log.timestamp.toLocaleTimeString()} + + + {log.platform} + + + {log.message} + +
+ ))} +
+
+
+ + + +
+ {logs.filter((log) => log.platform === 'roblox').map((log) => ( +
+ + {log.timestamp.toLocaleTimeString()} + + + {log.message} + +
+ ))} +
+
+
+ + + +
+ {logs.filter((log) => log.platform === 'web').map((log) => ( +
+ + {log.timestamp.toLocaleTimeString()} + + + {log.message} + +
+ ))} +
+
+
+ + + +
+ {logs.filter((log) => log.platform === 'mobile').map((log) => ( +
+ + {log.timestamp.toLocaleTimeString()} + + + {log.message} + +
+ ))} +
+
+
+
+
+ ); +} diff --git a/src/components/FileTabs.tsx b/src/components/FileTabs.tsx new file mode 100644 index 0000000..84f3051 --- /dev/null +++ b/src/components/FileTabs.tsx @@ -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 ( +
+
+ {openFiles.map((file) => ( +
onFileSelect(file)} + > + {file.name} + +
+ ))} +
+
+ ); +} diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx new file mode 100644 index 0000000..9cf2c5d --- /dev/null +++ b/src/components/FileTree.tsx @@ -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>(new Set(['root'])); + const [editingId, setEditingId] = useState(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 ( +
+
{ + if (node.type === 'folder') { + toggleFolder(node.id); + } else { + onFileSelect(node); + } + }} + > + {node.type === 'folder' ? ( + isExpanded ? ( + + ) : ( + + ) + ) : ( + + )} + + {isEditing ? ( + 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()} + /> + ) : ( + {node.name} + )} + + + e.stopPropagation()}> + + + + startRename(node)}> + + Rename + + handleDelete(node)} + className="text-destructive" + > + + Delete + + + +
+ + {node.type === 'folder' && isExpanded && node.children && ( +
{node.children.map((child) => renderNode(child, depth + 1))}
+ )} +
+ ); + }; + + return ( +
+
+ Explorer + +
+ +
{files.map((node) => renderNode(node))}
+
+
+ ); +} diff --git a/src/components/NewProjectModal.tsx b/src/components/NewProjectModal.tsx new file mode 100644 index 0000000..34628c1 --- /dev/null +++ b/src/components/NewProjectModal.tsx @@ -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 ( + + + + Create New Project + + +
+ {[1, 2, 3].map((s) => ( +
+
+ {s < step ? : s} +
+ {s < 3 && ( +
+ )} +
+ ))} +
+ + {step === 1 && ( +
+

Choose a Template

+
+ {templates.map((template) => ( + setSelectedTemplate(template.id)} + > +
+
{template.icon}
+ {template.popular && ( + + Popular + + )} +
+

{template.name}

+

+ {template.description} +

+
+ ))} +
+
+ + +
+
+ )} + + {step === 2 && ( +
+

Project Configuration

+
+
+ + setProjectName(e.target.value)} + placeholder="My Awesome Game" + className="mt-2" + /> +
+ +
+ +
+
+ + setPlatforms((p) => ({ ...p, roblox: checked as boolean })) + } + /> + +
+
+ + setPlatforms((p) => ({ ...p, web: checked as boolean })) + } + /> + +
+
+ + setPlatforms((p) => ({ ...p, mobile: checked as boolean })) + } + /> + +
+
+
+ +
+
+
+ + + + + + + +

+ Nexus Engine synchronizes game state across all platforms in real-time +

+
+
+
+
+ +
+ +
+
+ + + + + + + +

+ AeThex Passport provides unified identity across all your games +

+
+
+
+
+ +
+
+
+ +
+ +
+ + +
+
+
+ )} + + {step === 3 && ( +
+

Review & Create

+ +
+ +

{projectName}

+
+
+ +

+ {templates.find((t) => t.id === selectedTemplate)?.name} +

+
+
+ +
+ {platforms.roblox && Roblox} + {platforms.web && Web} + {platforms.mobile && Mobile} +
+
+
+ +
+ {nexusEngine && Nexus Engine} + {passportAuth && Passport Auth} + {!nexusEngine && !passportAuth && ( + None + )} +
+
+
+ +
+ +
+ + +
+
+
+ )} + +
+ ); +} diff --git a/src/components/PreviewModal.tsx b/src/components/PreviewModal.tsx new file mode 100644 index 0000000..9df7d03 --- /dev/null +++ b/src/components/PreviewModal.tsx @@ -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([ + { 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 ( + + +
+
+

Preview - All Platforms

+
+ + +
+
+ + + + All Platforms + Roblox + Web + Mobile + + + +
+ +
+

Roblox

+ +
+
+
+
🎮
+

Roblox Viewport

+

Studio integration

+
+
+
+ + +
+

Web Browser

+ +
+
+
+
🌐
+

Web Canvas

+

Browser preview

+
+
+
+ + +
+

Mobile

+ +
+
+
+
+
+
+
📱
+

Mobile View

+
+
+
+
+
+ +
+ + +
+

Shared State Sync

+ + Live Sync Enabled + +
+
+ + + + + + + + + + + + {sharedState.map((state, idx) => ( + + + + + + + + ))} + +
VariableRobloxWebMobileStatus
{state.variable}{state.roblox}{state.web}{state.mobile} + + {getStatusIcon(state.status)} {state.status} + +
+
+
+ + + + +
+
🎮
+

Roblox Preview

+

Full-screen Roblox viewport

+ +
+
+
+ + + +
+
🌐
+

Web Preview

+

Browser-based game preview

+ +
+
+
+ + +
+
+ +
+
📱
+

Mobile Preview

+

iOS/Android view

+ +
+
+
+
+ + +
+ +
+ ); +} diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index c52ebf6..15217bb 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -1,17 +1,32 @@ import { Button } from '@/components/ui/button'; 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 { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; interface ToolbarProps { code: string; 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 [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(null); + + useEffect(() => { + window.spark.user().then(setUser).catch(() => setUser(null)); + }, []); const handleCopy = async () => { try { @@ -41,10 +56,40 @@ export function Toolbar({ code, onTemplatesClick }: ToolbarProps) {

AeThex

+ Studio
+ + + + + Create a new project + + + + + + + Preview all platforms + + + + + {user && ( + <> +
+

{user.login}

+

{user.email}

+
+ + + )} + toast.info('Profile coming soon!')}> + + Profile + + toast.info('Sign out feature coming soon!')}> + + Sign Out + +
+