您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

code-panel.tsx 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /* eslint-disable @typescript-eslint/ban-ts-comment */
  2. import styled from "styles"
  3. import { useStateDesigner } from "@state-designer/react"
  4. import React, { useEffect, useRef } from "react"
  5. import { motion } from "framer-motion"
  6. import state, { useSelector } from "state"
  7. import { CodeFile } from "types"
  8. import CodeDocs from "./code-docs"
  9. import CodeEditor from "./code-editor"
  10. import { getShapesFromCode } from "lib/code/generate"
  11. import {
  12. X,
  13. Code,
  14. Info,
  15. PlayCircle,
  16. ChevronUp,
  17. ChevronDown,
  18. } from "react-feather"
  19. const getErrorLineAndColumn = (e: any) => {
  20. if ("line" in e) {
  21. return { line: Number(e.line), column: e.column }
  22. }
  23. const result = e.stack.match(/:([0-9]+):([0-9]+)/)
  24. if (result) {
  25. return { line: Number(result[1]) - 1, column: result[2] }
  26. }
  27. }
  28. export default function CodePanel() {
  29. const rContainer = useRef<HTMLDivElement>(null)
  30. const fileId = "file0"
  31. const isReadOnly = useSelector((s) => s.data.isReadOnly)
  32. const file = useSelector((s) => s.data.document.code[fileId])
  33. const isOpen = true
  34. const fontSize = useSelector((s) => s.data.settings.fontSize)
  35. const local = useStateDesigner({
  36. data: {
  37. code: file.code,
  38. error: null as { message: string; line: number; column: number } | null,
  39. },
  40. on: {
  41. MOUNTED: "setCode",
  42. CHANGED_FILE: "loadFile",
  43. },
  44. initial: "editingCode",
  45. states: {
  46. editingCode: {
  47. on: {
  48. RAN_CODE: "runCode",
  49. SAVED_CODE: ["runCode", "saveCode"],
  50. CHANGED_CODE: [{ secretlyDo: "setCode" }],
  51. CLEARED_ERROR: { if: "hasError", do: "clearError" },
  52. TOGGLED_DOCS: { to: "viewingDocs" },
  53. },
  54. },
  55. viewingDocs: {
  56. on: {
  57. TOGGLED_DOCS: { to: "editingCode" },
  58. },
  59. },
  60. },
  61. conditions: {
  62. hasError(data) {
  63. return !!data.error
  64. },
  65. },
  66. actions: {
  67. loadFile(data, payload: { file: CodeFile }) {
  68. data.code = payload.file.code
  69. },
  70. setCode(data, payload: { code: string }) {
  71. data.code = payload.code
  72. },
  73. runCode(data) {
  74. let error = null
  75. try {
  76. const shapes = getShapesFromCode(data.code)
  77. state.send("GENERATED_SHAPES_FROM_CODE", { shapes })
  78. } catch (e) {
  79. console.error(e)
  80. error = { message: e.message, ...getErrorLineAndColumn(e) }
  81. }
  82. data.error = error
  83. },
  84. saveCode(data) {
  85. state.send("CHANGED_CODE", { fileId, code: data.code })
  86. },
  87. clearError(data) {
  88. data.error = null
  89. },
  90. },
  91. })
  92. useEffect(() => {
  93. local.send("CHANGED_FILE", { file })
  94. }, [file])
  95. useEffect(() => {
  96. local.send("MOUNTED", { code: state.data.document.code[fileId].code })
  97. return () => {
  98. state.send("CHANGED_CODE", { fileId, code: local.data.code })
  99. }
  100. }, [])
  101. const { error } = local.data
  102. return (
  103. <PanelContainer
  104. data-bp-desktop
  105. ref={rContainer}
  106. dragMomentum={false}
  107. isCollapsed={!isOpen}
  108. >
  109. {isOpen ? (
  110. <Content>
  111. <Header>
  112. <IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}>
  113. <X />
  114. </IconButton>
  115. <h3>Code</h3>
  116. <ButtonsGroup>
  117. <FontSizeButtons>
  118. <IconButton
  119. disabled={!local.isIn("editingCode")}
  120. onClick={() => state.send("INCREASED_CODE_FONT_SIZE")}
  121. >
  122. <ChevronUp />
  123. </IconButton>
  124. <IconButton
  125. disabled={!local.isIn("editingCode")}
  126. onClick={() => state.send("DECREASED_CODE_FONT_SIZE")}
  127. >
  128. <ChevronDown />
  129. </IconButton>
  130. </FontSizeButtons>
  131. <IconButton onClick={() => local.send("TOGGLED_DOCS")}>
  132. <Info />
  133. </IconButton>
  134. <IconButton
  135. disabled={!local.isIn("editingCode")}
  136. onClick={() => local.send("SAVED_CODE")}
  137. >
  138. <PlayCircle />
  139. </IconButton>
  140. </ButtonsGroup>
  141. </Header>
  142. <EditorContainer>
  143. <CodeEditor
  144. fontSize={fontSize}
  145. readOnly={isReadOnly}
  146. value={file.code}
  147. error={error}
  148. onChange={(code) => local.send("CHANGED_CODE", { code })}
  149. onSave={() => local.send("SAVED_CODE")}
  150. onKey={() => local.send("CLEARED_ERROR")}
  151. />
  152. <CodeDocs isHidden={!local.isIn("viewingDocs")} />
  153. </EditorContainer>
  154. <ErrorContainer>
  155. {error &&
  156. (error.line
  157. ? `(${Number(error.line) - 2}:${error.column}) ${error.message}`
  158. : error.message)}
  159. </ErrorContainer>
  160. </Content>
  161. ) : (
  162. <IconButton onClick={() => state.send("OPENED_CODE_PANEL")}>
  163. <Code />
  164. </IconButton>
  165. )}
  166. </PanelContainer>
  167. )
  168. }
  169. const PanelContainer = styled(motion.div, {
  170. position: "absolute",
  171. top: "8px",
  172. right: "8px",
  173. bottom: "48px",
  174. backgroundColor: "$panel",
  175. borderRadius: "4px",
  176. overflow: "hidden",
  177. border: "1px solid $border",
  178. pointerEvents: "all",
  179. userSelect: "none",
  180. zIndex: 200,
  181. button: {
  182. border: "none",
  183. },
  184. variants: {
  185. isCollapsed: {
  186. true: {},
  187. false: {},
  188. },
  189. },
  190. })
  191. const IconButton = styled("button", {
  192. height: "40px",
  193. width: "40px",
  194. backgroundColor: "$panel",
  195. borderRadius: "4px",
  196. border: "1px solid $border",
  197. padding: "0",
  198. margin: "0",
  199. display: "flex",
  200. alignItems: "center",
  201. justifyContent: "center",
  202. outline: "none",
  203. pointerEvents: "all",
  204. cursor: "pointer",
  205. "&:hover:not(:disabled)": {
  206. backgroundColor: "$panel",
  207. },
  208. "&:disabled": {
  209. opacity: "0.5",
  210. },
  211. svg: {
  212. height: "20px",
  213. width: "20px",
  214. strokeWidth: "2px",
  215. stroke: "$text",
  216. },
  217. })
  218. const Content = styled("div", {
  219. display: "grid",
  220. gridTemplateColumns: "1fr",
  221. gridTemplateRows: "auto 1fr 28px",
  222. height: "100%",
  223. width: 560,
  224. minWidth: "100%",
  225. maxWidth: 560,
  226. overflow: "hidden",
  227. userSelect: "none",
  228. pointerEvents: "all",
  229. })
  230. const Header = styled("div", {
  231. pointerEvents: "all",
  232. display: "grid",
  233. gridTemplateColumns: "auto 1fr",
  234. alignItems: "center",
  235. justifyContent: "center",
  236. borderBottom: "1px solid $border",
  237. "& button": {
  238. gridColumn: "1",
  239. gridRow: "1",
  240. },
  241. "& h3": {
  242. gridColumn: "1 / span 3",
  243. gridRow: "1",
  244. textAlign: "center",
  245. margin: "0",
  246. padding: "0",
  247. fontSize: "16px",
  248. },
  249. })
  250. const ButtonsGroup = styled("div", {
  251. gridRow: "1",
  252. gridColumn: "3",
  253. display: "flex",
  254. })
  255. const EditorContainer = styled("div", {
  256. position: "relative",
  257. pointerEvents: "all",
  258. overflowY: "scroll",
  259. })
  260. const ErrorContainer = styled("div", {
  261. overflowX: "scroll",
  262. color: "$text",
  263. font: "$debug",
  264. padding: "0 12px",
  265. display: "flex",
  266. alignItems: "center",
  267. })
  268. const FontSizeButtons = styled("div", {
  269. paddingRight: 4,
  270. "& > button": {
  271. height: "50%",
  272. width: "100%",
  273. "&:nth-of-type(1)": {
  274. paddingTop: 4,
  275. },
  276. "&:nth-of-type(2)": {
  277. paddingBottom: 4,
  278. },
  279. "& svg": {
  280. height: 12,
  281. },
  282. },
  283. })