mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-27 07:14:56 +00:00
refactor: modularize lua blocks editor
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import { Box, Button, Card, CardContent, CardHeader, Stack, TextField, Typography } from '@mui/material'
|
||||
import { Add as AddIcon } from '@mui/icons-material'
|
||||
import type { LuaScript } from '@/lib/level-types'
|
||||
import type { BlockDefinition, BlockSlot, LuaBlock, LuaBlockType } from './types'
|
||||
import { BlockList } from './blocks/BlockList'
|
||||
import styles from './LuaBlocksEditor.module.scss'
|
||||
|
||||
interface BlockListViewProps {
|
||||
activeBlocks: LuaBlock[]
|
||||
blockDefinitionMap: Map<LuaBlockType, BlockDefinition>
|
||||
onRequestAddBlock: (
|
||||
event: MouseEvent<HTMLElement>,
|
||||
target: { parentId: string | null; slot: BlockSlot }
|
||||
) => void
|
||||
onMoveBlock: (blockId: string, direction: 'up' | 'down') => void
|
||||
onDuplicateBlock: (blockId: string) => void
|
||||
onRemoveBlock: (blockId: string) => void
|
||||
onUpdateField: (blockId: string, fieldName: string, value: string) => void
|
||||
onUpdateScript: (updates: Partial<LuaScript>) => void
|
||||
selectedScript: LuaScript | null
|
||||
}
|
||||
|
||||
export function BlockListView({
|
||||
activeBlocks,
|
||||
blockDefinitionMap,
|
||||
onRequestAddBlock,
|
||||
onMoveBlock,
|
||||
onDuplicateBlock,
|
||||
onRemoveBlock,
|
||||
onUpdateField,
|
||||
onUpdateScript,
|
||||
selectedScript,
|
||||
}: BlockListViewProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Block workspace"
|
||||
subheader="Stack blocks to generate Lua code"
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={(event) => onRequestAddBlock(event, { parentId: null, slot: 'root' })}
|
||||
disabled={!selectedScript}
|
||||
>
|
||||
Add block
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
{!selectedScript ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Select a script to start building blocks.
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={2} direction={{ xs: 'column', md: 'row' }}>
|
||||
<TextField
|
||||
label="Script name"
|
||||
value={selectedScript.name}
|
||||
onChange={(event) => onUpdateScript({ name: event.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
value={selectedScript.description || ''}
|
||||
onChange={(event) => onUpdateScript({ description: event.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
<Box className={styles.workspaceSurface}>
|
||||
{activeBlocks.length > 0 ? (
|
||||
<BlockList
|
||||
blocks={activeBlocks}
|
||||
blockDefinitionMap={blockDefinitionMap}
|
||||
onRequestAddBlock={onRequestAddBlock}
|
||||
onMoveBlock={onMoveBlock}
|
||||
onDuplicateBlock={onDuplicateBlock}
|
||||
onRemoveBlock={onRemoveBlock}
|
||||
onUpdateField={onUpdateField}
|
||||
/>
|
||||
) : (
|
||||
<Box className={styles.blockEmpty}>Add a block to start building Lua logic.</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Blocks are saved in the script as metadata, so you can reload them later.
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
73
frontends/nextjs/src/components/editors/lua/CodePreview.tsx
Normal file
73
frontends/nextjs/src/components/editors/lua/CodePreview.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Box, Button, Card, CardContent, CardHeader, Stack, Tooltip } from '@mui/material'
|
||||
import { ContentCopy, Refresh as RefreshIcon, Save as SaveIcon } from '@mui/icons-material'
|
||||
import type { LuaScript } from '@/lib/level-types'
|
||||
import styles from './LuaBlocksEditor.module.scss'
|
||||
|
||||
interface CodePreviewProps {
|
||||
generatedCode: string
|
||||
onApplyCode: () => void
|
||||
onCopyCode: () => void
|
||||
onReloadFromCode: () => void
|
||||
selectedScript: LuaScript | null
|
||||
}
|
||||
|
||||
export function CodePreview({
|
||||
generatedCode,
|
||||
onApplyCode,
|
||||
onCopyCode,
|
||||
onReloadFromCode,
|
||||
selectedScript,
|
||||
}: CodePreviewProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Lua preview"
|
||||
subheader="Generated code from your blocks"
|
||||
action={
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Tooltip title="Reload blocks from script">
|
||||
<span>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon fontSize="small" />}
|
||||
onClick={onReloadFromCode}
|
||||
disabled={!selectedScript}
|
||||
>
|
||||
Reload
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Copy code">
|
||||
<span>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<ContentCopy fontSize="small" />}
|
||||
onClick={onCopyCode}
|
||||
disabled={!selectedScript}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<SaveIcon fontSize="small" />}
|
||||
onClick={onApplyCode}
|
||||
disabled={!selectedScript}
|
||||
>
|
||||
Apply to script
|
||||
</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box className={styles.codePreview}>
|
||||
<pre>{generatedCode}</pre>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -5,27 +5,21 @@ import {
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Divider,
|
||||
IconButton,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Stack,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Add as AddIcon,
|
||||
ContentCopy,
|
||||
Delete as DeleteIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Save as SaveIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material'
|
||||
import type { LuaScript } from '@/lib/level-types'
|
||||
import { BlockList } from './blocks/BlockList'
|
||||
import { BlockMenu } from './blocks/BlockMenu'
|
||||
import { useBlockDefinitions } from './hooks/useBlockDefinitions'
|
||||
import { useLuaBlocksState } from './hooks/useLuaBlocksState'
|
||||
import { BlockListView } from './BlockListView'
|
||||
import { CodePreview } from './CodePreview'
|
||||
import { useLuaBlockEditorState } from './hooks/useLuaBlockEditorState'
|
||||
import styles from './LuaBlocksEditor.module.scss'
|
||||
|
||||
interface LuaBlocksEditorProps {
|
||||
@@ -34,18 +28,11 @@ interface LuaBlocksEditorProps {
|
||||
}
|
||||
|
||||
export function LuaBlocksEditor({ scripts, onScriptsChange }: LuaBlocksEditorProps) {
|
||||
const {
|
||||
blockDefinitions,
|
||||
blockDefinitionMap,
|
||||
blocksByCategory,
|
||||
createBlock,
|
||||
cloneBlock,
|
||||
buildLuaFromBlocks,
|
||||
decodeBlocksMetadata,
|
||||
} = useBlockDefinitions()
|
||||
|
||||
const {
|
||||
activeBlocks,
|
||||
blockDefinitionMap,
|
||||
blockDefinitions,
|
||||
blocksByCategory,
|
||||
generatedCode,
|
||||
handleAddBlock,
|
||||
handleAddScript,
|
||||
@@ -64,173 +51,7 @@ export function LuaBlocksEditor({ scripts, onScriptsChange }: LuaBlocksEditorPro
|
||||
selectedScript,
|
||||
selectedScriptId,
|
||||
setSelectedScriptId,
|
||||
} = useLuaBlocksState({
|
||||
scripts,
|
||||
onScriptsChange,
|
||||
buildLuaFromBlocks,
|
||||
createBlock,
|
||||
cloneBlock,
|
||||
decodeBlocksMetadata,
|
||||
})
|
||||
|
||||
const renderBlockLibrary = () => (
|
||||
<Card>
|
||||
<CardHeader title="Block library" subheader="Click a block to add it" />
|
||||
<CardContent>
|
||||
<Stack spacing={2}>
|
||||
{Object.entries(blocksByCategory).map(([category, blocks]) => (
|
||||
<Box key={category}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||
{category}
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{blocks.map((block) => (
|
||||
<Paper
|
||||
key={block.type}
|
||||
className={styles.libraryBlock}
|
||||
data-category={block.category}
|
||||
onClick={() => handleAddBlock(block.type, { parentId: null, slot: 'root' })}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography className={styles.libraryBlockTitle}>{block.label}</Typography>
|
||||
<Typography className={styles.libraryBlockDesc}>{block.description}</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleAddBlock(block.type, { parentId: null, slot: 'root' })
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
const renderWorkspace = () => (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Block workspace"
|
||||
subheader="Stack blocks to generate Lua code"
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={(event) => handleRequestAddBlock(event, { parentId: null, slot: 'root' })}
|
||||
disabled={!selectedScript}
|
||||
>
|
||||
Add block
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
{!selectedScript ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Select a script to start building blocks.
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={2} direction={{ xs: 'column', md: 'row' }}>
|
||||
<TextField
|
||||
label="Script name"
|
||||
value={selectedScript.name}
|
||||
onChange={(event) => handleUpdateScript({ name: event.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
value={selectedScript.description || ''}
|
||||
onChange={(event) => handleUpdateScript({ description: event.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
<Box className={styles.workspaceSurface}>
|
||||
{activeBlocks.length > 0 ? (
|
||||
<BlockList
|
||||
blocks={activeBlocks}
|
||||
blockDefinitionMap={blockDefinitionMap}
|
||||
onRequestAddBlock={handleRequestAddBlock}
|
||||
onMoveBlock={handleMoveBlock}
|
||||
onDuplicateBlock={handleDuplicateBlock}
|
||||
onRemoveBlock={handleRemoveBlock}
|
||||
onUpdateField={handleUpdateField}
|
||||
/>
|
||||
) : (
|
||||
<Box className={styles.blockEmpty}>Add a block to start building Lua logic.</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Blocks are saved in the script as metadata, so you can reload them later.
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
const renderScriptList = () => (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Lua block scripts"
|
||||
subheader="Create scripts using Scratch-style blocks"
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={2}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={handleAddScript}>
|
||||
New block script
|
||||
</Button>
|
||||
<Divider />
|
||||
<List disablePadding>
|
||||
{scripts.length === 0 && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No scripts yet. Create a block script to begin.
|
||||
</Typography>
|
||||
)}
|
||||
{scripts.map((script) => (
|
||||
<ListItemButton
|
||||
key={script.id}
|
||||
selected={script.id === selectedScriptId}
|
||||
onClick={() => setSelectedScriptId(script.id)}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
mb: 1,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={script.name}
|
||||
secondary={script.description || 'No description'}
|
||||
primaryTypographyProps={{ fontWeight: 600 }}
|
||||
secondaryTypographyProps={{ variant: 'caption' }}
|
||||
/>
|
||||
<Tooltip title="Delete script">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleDeleteScript(script.id)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
} = useLuaBlockEditorState({ scripts, onScriptsChange })
|
||||
|
||||
return (
|
||||
<Box className={styles.root}>
|
||||
@@ -242,55 +63,121 @@ export function LuaBlocksEditor({ scripts, onScriptsChange }: LuaBlocksEditorPro
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
{renderScriptList()}
|
||||
{renderBlockLibrary()}
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Lua block scripts"
|
||||
subheader="Create scripts using Scratch-style blocks"
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={2}>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={handleAddScript}>
|
||||
New block script
|
||||
</Button>
|
||||
<Divider />
|
||||
<List disablePadding>
|
||||
{scripts.length === 0 && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No scripts yet. Create a block script to begin.
|
||||
</Typography>
|
||||
)}
|
||||
{scripts.map((script) => (
|
||||
<ListItemButton
|
||||
key={script.id}
|
||||
selected={script.id === selectedScriptId}
|
||||
onClick={() => setSelectedScriptId(script.id)}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
mb: 1,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={script.name}
|
||||
secondary={script.description || 'No description'}
|
||||
primaryTypographyProps={{ fontWeight: 600 }}
|
||||
secondaryTypographyProps={{ variant: 'caption' }}
|
||||
/>
|
||||
<Tooltip title="Delete script">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleDeleteScript(script.id)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader title="Block library" subheader="Click a block to add it" />
|
||||
<CardContent>
|
||||
<Stack spacing={2}>
|
||||
{Object.entries(blocksByCategory).map(([category, blocks]) => (
|
||||
<Box key={category}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||
{category}
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{blocks.map((block) => (
|
||||
<Paper
|
||||
key={block.type}
|
||||
className={styles.libraryBlock}
|
||||
data-category={block.category}
|
||||
onClick={() => handleAddBlock(block.type, { parentId: null, slot: 'root' })}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography className={styles.libraryBlockTitle}>{block.label}</Typography>
|
||||
<Typography className={styles.libraryBlockDesc}>{block.description}</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleAddBlock(block.type, { parentId: null, slot: 'root' })
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={3}>
|
||||
{renderWorkspace()}
|
||||
<BlockListView
|
||||
activeBlocks={activeBlocks}
|
||||
blockDefinitionMap={blockDefinitionMap}
|
||||
onRequestAddBlock={handleRequestAddBlock}
|
||||
onMoveBlock={handleMoveBlock}
|
||||
onDuplicateBlock={handleDuplicateBlock}
|
||||
onRemoveBlock={handleRemoveBlock}
|
||||
onUpdateField={handleUpdateField}
|
||||
onUpdateScript={handleUpdateScript}
|
||||
selectedScript={selectedScript}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Lua preview"
|
||||
subheader="Generated code from your blocks"
|
||||
action={
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Tooltip title="Reload blocks from script">
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleReloadFromCode}
|
||||
disabled={!selectedScript}
|
||||
>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Copy code">
|
||||
<span>
|
||||
<IconButton size="small" onClick={handleCopyCode} disabled={!selectedScript}>
|
||||
<ContentCopy fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<SaveIcon fontSize="small" />}
|
||||
onClick={handleApplyCode}
|
||||
disabled={!selectedScript}
|
||||
>
|
||||
Apply to script
|
||||
</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box className={styles.codePreview}>
|
||||
<pre>{generatedCode}</pre>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<CodePreview
|
||||
generatedCode={generatedCode}
|
||||
onApplyCode={handleApplyCode}
|
||||
onCopyCode={handleCopyCode}
|
||||
onReloadFromCode={handleReloadFromCode}
|
||||
selectedScript={selectedScript}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { LuaScript } from '@/lib/level-types'
|
||||
import { useBlockDefinitions } from './useBlockDefinitions'
|
||||
import { useLuaBlocksState } from './useLuaBlocksState'
|
||||
|
||||
interface UseLuaBlockEditorStateProps {
|
||||
scripts: LuaScript[]
|
||||
onScriptsChange: (scripts: LuaScript[]) => void
|
||||
}
|
||||
|
||||
export function useLuaBlockEditorState({ scripts, onScriptsChange }: UseLuaBlockEditorStateProps) {
|
||||
const blockDefinitionState = useBlockDefinitions()
|
||||
|
||||
const luaBlockState = useLuaBlocksState({
|
||||
scripts,
|
||||
onScriptsChange,
|
||||
buildLuaFromBlocks: blockDefinitionState.buildLuaFromBlocks,
|
||||
createBlock: blockDefinitionState.createBlock,
|
||||
cloneBlock: blockDefinitionState.cloneBlock,
|
||||
decodeBlocksMetadata: blockDefinitionState.decodeBlocksMetadata,
|
||||
})
|
||||
|
||||
return {
|
||||
...blockDefinitionState,
|
||||
...luaBlockState,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user