refactor: modularize lua blocks editor

This commit is contained in:
2025-12-27 18:40:43 +00:00
parent cadaa8c5fe
commit 7c061b43ca
4 changed files with 314 additions and 233 deletions

View File

@@ -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>
)
}

View 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>
)
}

View File

@@ -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>

View File

@@ -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,
}
}