313 lines
7.1 KiB
TypeScript
313 lines
7.1 KiB
TypeScript
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
import styled from "styles"
|
|
import { useStateDesigner } from "@state-designer/react"
|
|
import React, { useEffect, useRef } from "react"
|
|
import { motion } from "framer-motion"
|
|
import state, { useSelector } from "state"
|
|
import { CodeFile } from "types"
|
|
import CodeDocs from "./code-docs"
|
|
import CodeEditor from "./code-editor"
|
|
import { getShapesFromCode } from "lib/code/generate"
|
|
import {
|
|
X,
|
|
Code,
|
|
Info,
|
|
PlayCircle,
|
|
ChevronUp,
|
|
ChevronDown,
|
|
} from "react-feather"
|
|
|
|
const getErrorLineAndColumn = (e: any) => {
|
|
if ("line" in e) {
|
|
return { line: Number(e.line), column: e.column }
|
|
}
|
|
|
|
const result = e.stack.match(/:([0-9]+):([0-9]+)/)
|
|
if (result) {
|
|
return { line: Number(result[1]) - 1, column: result[2] }
|
|
}
|
|
}
|
|
|
|
export default function CodePanel() {
|
|
const rContainer = useRef<HTMLDivElement>(null)
|
|
|
|
const fileId = "file0"
|
|
const isReadOnly = useSelector((s) => s.data.isReadOnly)
|
|
const file = useSelector((s) => s.data.document.code[fileId])
|
|
const isOpen = true
|
|
const fontSize = useSelector((s) => s.data.settings.fontSize)
|
|
|
|
const local = useStateDesigner({
|
|
data: {
|
|
code: file.code,
|
|
error: null as { message: string; line: number; column: number } | null,
|
|
},
|
|
on: {
|
|
MOUNTED: "setCode",
|
|
CHANGED_FILE: "loadFile",
|
|
},
|
|
initial: "editingCode",
|
|
states: {
|
|
editingCode: {
|
|
on: {
|
|
RAN_CODE: "runCode",
|
|
SAVED_CODE: ["runCode", "saveCode"],
|
|
CHANGED_CODE: [{ secretlyDo: "setCode" }],
|
|
CLEARED_ERROR: { if: "hasError", do: "clearError" },
|
|
TOGGLED_DOCS: { to: "viewingDocs" },
|
|
},
|
|
},
|
|
viewingDocs: {
|
|
on: {
|
|
TOGGLED_DOCS: { to: "editingCode" },
|
|
},
|
|
},
|
|
},
|
|
conditions: {
|
|
hasError(data) {
|
|
return !!data.error
|
|
},
|
|
},
|
|
actions: {
|
|
loadFile(data, payload: { file: CodeFile }) {
|
|
data.code = payload.file.code
|
|
},
|
|
setCode(data, payload: { code: string }) {
|
|
data.code = payload.code
|
|
},
|
|
runCode(data) {
|
|
let error = null
|
|
|
|
try {
|
|
const shapes = getShapesFromCode(data.code)
|
|
state.send("GENERATED_SHAPES_FROM_CODE", { shapes })
|
|
} catch (e) {
|
|
console.error(e)
|
|
error = { message: e.message, ...getErrorLineAndColumn(e) }
|
|
}
|
|
|
|
data.error = error
|
|
},
|
|
saveCode(data) {
|
|
state.send("CHANGED_CODE", { fileId, code: data.code })
|
|
},
|
|
clearError(data) {
|
|
data.error = null
|
|
},
|
|
},
|
|
})
|
|
|
|
useEffect(() => {
|
|
local.send("CHANGED_FILE", { file })
|
|
}, [file])
|
|
|
|
useEffect(() => {
|
|
local.send("MOUNTED", { code: state.data.document.code[fileId].code })
|
|
return () => {
|
|
state.send("CHANGED_CODE", { fileId, code: local.data.code })
|
|
}
|
|
}, [])
|
|
|
|
const { error } = local.data
|
|
|
|
return (
|
|
<PanelContainer
|
|
data-bp-desktop
|
|
ref={rContainer}
|
|
dragMomentum={false}
|
|
isCollapsed={!isOpen}
|
|
>
|
|
{isOpen ? (
|
|
<Content>
|
|
<Header>
|
|
<IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}>
|
|
<X />
|
|
</IconButton>
|
|
<h3>Code</h3>
|
|
<ButtonsGroup>
|
|
<FontSizeButtons>
|
|
<IconButton
|
|
disabled={!local.isIn("editingCode")}
|
|
onClick={() => state.send("INCREASED_CODE_FONT_SIZE")}
|
|
>
|
|
<ChevronUp />
|
|
</IconButton>
|
|
<IconButton
|
|
disabled={!local.isIn("editingCode")}
|
|
onClick={() => state.send("DECREASED_CODE_FONT_SIZE")}
|
|
>
|
|
<ChevronDown />
|
|
</IconButton>
|
|
</FontSizeButtons>
|
|
<IconButton onClick={() => local.send("TOGGLED_DOCS")}>
|
|
<Info />
|
|
</IconButton>
|
|
<IconButton
|
|
disabled={!local.isIn("editingCode")}
|
|
onClick={() => local.send("SAVED_CODE")}
|
|
>
|
|
<PlayCircle />
|
|
</IconButton>
|
|
</ButtonsGroup>
|
|
</Header>
|
|
<EditorContainer>
|
|
<CodeEditor
|
|
fontSize={fontSize}
|
|
readOnly={isReadOnly}
|
|
value={file.code}
|
|
error={error}
|
|
onChange={(code) => local.send("CHANGED_CODE", { code })}
|
|
onSave={() => local.send("SAVED_CODE")}
|
|
onKey={() => local.send("CLEARED_ERROR")}
|
|
/>
|
|
<CodeDocs isHidden={!local.isIn("viewingDocs")} />
|
|
</EditorContainer>
|
|
<ErrorContainer>
|
|
{error &&
|
|
(error.line
|
|
? `(${Number(error.line) - 2}:${error.column}) ${error.message}`
|
|
: error.message)}
|
|
</ErrorContainer>
|
|
</Content>
|
|
) : (
|
|
<IconButton onClick={() => state.send("OPENED_CODE_PANEL")}>
|
|
<Code />
|
|
</IconButton>
|
|
)}
|
|
</PanelContainer>
|
|
)
|
|
}
|
|
|
|
const PanelContainer = styled(motion.div, {
|
|
position: "absolute",
|
|
top: "48px",
|
|
right: "8px",
|
|
bottom: "48px",
|
|
backgroundColor: "$panel",
|
|
borderRadius: "4px",
|
|
overflow: "hidden",
|
|
border: "1px solid $border",
|
|
pointerEvents: "all",
|
|
userSelect: "none",
|
|
zIndex: 200,
|
|
|
|
button: {
|
|
border: "none",
|
|
},
|
|
|
|
variants: {
|
|
isCollapsed: {
|
|
true: {},
|
|
false: {},
|
|
},
|
|
},
|
|
})
|
|
|
|
const IconButton = styled("button", {
|
|
height: "40px",
|
|
width: "40px",
|
|
backgroundColor: "$panel",
|
|
borderRadius: "4px",
|
|
border: "1px solid $border",
|
|
padding: "0",
|
|
margin: "0",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
outline: "none",
|
|
pointerEvents: "all",
|
|
cursor: "pointer",
|
|
|
|
"&:hover:not(:disabled)": {
|
|
backgroundColor: "$panel",
|
|
},
|
|
|
|
"&:disabled": {
|
|
opacity: "0.5",
|
|
},
|
|
|
|
svg: {
|
|
height: "20px",
|
|
width: "20px",
|
|
strokeWidth: "2px",
|
|
stroke: "$text",
|
|
},
|
|
})
|
|
|
|
const Content = styled("div", {
|
|
display: "grid",
|
|
gridTemplateColumns: "1fr",
|
|
gridTemplateRows: "auto 1fr 28px",
|
|
height: "100%",
|
|
width: 560,
|
|
minWidth: "100%",
|
|
maxWidth: 560,
|
|
overflow: "hidden",
|
|
userSelect: "none",
|
|
pointerEvents: "all",
|
|
})
|
|
|
|
const Header = styled("div", {
|
|
pointerEvents: "all",
|
|
display: "grid",
|
|
gridTemplateColumns: "auto 1fr",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
borderBottom: "1px solid $border",
|
|
|
|
"& button": {
|
|
gridColumn: "1",
|
|
gridRow: "1",
|
|
},
|
|
|
|
"& h3": {
|
|
gridColumn: "1 / span 3",
|
|
gridRow: "1",
|
|
textAlign: "center",
|
|
margin: "0",
|
|
padding: "0",
|
|
fontSize: "16px",
|
|
},
|
|
})
|
|
|
|
const ButtonsGroup = styled("div", {
|
|
gridRow: "1",
|
|
gridColumn: "3",
|
|
display: "flex",
|
|
})
|
|
|
|
const EditorContainer = styled("div", {
|
|
position: "relative",
|
|
pointerEvents: "all",
|
|
overflowY: "scroll",
|
|
})
|
|
|
|
const ErrorContainer = styled("div", {
|
|
overflowX: "scroll",
|
|
color: "$text",
|
|
font: "$debug",
|
|
padding: "0 12px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
})
|
|
|
|
const FontSizeButtons = styled("div", {
|
|
paddingRight: 4,
|
|
|
|
"& > button": {
|
|
height: "50%",
|
|
width: "100%",
|
|
|
|
"&:nth-of-type(1)": {
|
|
paddingTop: 4,
|
|
},
|
|
|
|
"&:nth-of-type(2)": {
|
|
paddingBottom: 4,
|
|
},
|
|
|
|
"& svg": {
|
|
height: 12,
|
|
},
|
|
},
|
|
})
|