Ver código fonte

adds hit testing for hovers

main
Steve Ruiz 4 anos atrás
pai
commit
afa8f53dff

+ 2
- 2
components/canvas/bounds-bg.tsx Ver arquivo

@@ -16,8 +16,8 @@ export default function BoundsBg() {
16 16
       ref={rBounds}
17 17
       x={minX}
18 18
       y={minY}
19
-      width={width}
20
-      height={height}
19
+      width={Math.max(1, width)}
20
+      height={Math.max(1, height)}
21 21
       onPointerDown={(e) => {
22 22
         if (e.buttons !== 1) return
23 23
         e.stopPropagation()

+ 57
- 62
components/canvas/bounds.tsx Ver arquivo

@@ -16,8 +16,6 @@ export default function Bounds() {
16 16
   const p = 4 / zoom
17 17
   const cp = p * 2
18 18
 
19
-  if (width < p || height < p) return null
20
-
21 19
   return (
22 20
     <g pointerEvents={isBrushing ? "none" : "all"}>
23 21
       <StyledBounds
@@ -27,66 +25,62 @@ export default function Bounds() {
27 25
         height={height}
28 26
         pointerEvents="none"
29 27
       />
30
-      {width * zoom > 8 && height * zoom > 8 && (
31
-        <>
32
-          <EdgeHorizontal
33
-            x={minX + p}
34
-            y={minY}
35
-            width={Math.max(0, width - p * 2)}
36
-            height={p}
37
-            edge={TransformEdge.Top}
38
-          />
39
-          <EdgeVertical
40
-            x={maxX}
41
-            y={minY + p}
42
-            width={p}
43
-            height={Math.max(0, height - p * 2)}
44
-            edge={TransformEdge.Right}
45
-          />
46
-          <EdgeHorizontal
47
-            x={minX + p}
48
-            y={maxY}
49
-            width={Math.max(0, width - p * 2)}
50
-            height={p}
51
-            edge={TransformEdge.Bottom}
52
-          />
53
-          <EdgeVertical
54
-            x={minX}
55
-            y={minY + p}
56
-            width={p}
57
-            height={Math.max(0, height - p * 2)}
58
-            edge={TransformEdge.Left}
59
-          />
60
-          <Corner
61
-            x={minX}
62
-            y={minY}
63
-            width={cp}
64
-            height={cp}
65
-            corner={TransformCorner.TopLeft}
66
-          />
67
-          <Corner
68
-            x={maxX}
69
-            y={minY}
70
-            width={cp}
71
-            height={cp}
72
-            corner={TransformCorner.TopRight}
73
-          />
74
-          <Corner
75
-            x={maxX}
76
-            y={maxY}
77
-            width={cp}
78
-            height={cp}
79
-            corner={TransformCorner.BottomRight}
80
-          />
81
-          <Corner
82
-            x={minX}
83
-            y={maxY}
84
-            width={cp}
85
-            height={cp}
86
-            corner={TransformCorner.BottomLeft}
87
-          />
88
-        </>
89
-      )}
28
+      <EdgeHorizontal
29
+        x={minX + p}
30
+        y={minY}
31
+        width={Math.max(0, width - p * 2)}
32
+        height={p}
33
+        edge={TransformEdge.Top}
34
+      />
35
+      <EdgeVertical
36
+        x={maxX}
37
+        y={minY + p}
38
+        width={p}
39
+        height={Math.max(0, height - p * 2)}
40
+        edge={TransformEdge.Right}
41
+      />
42
+      <EdgeHorizontal
43
+        x={minX + p}
44
+        y={maxY}
45
+        width={Math.max(0, width - p * 2)}
46
+        height={p}
47
+        edge={TransformEdge.Bottom}
48
+      />
49
+      <EdgeVertical
50
+        x={minX}
51
+        y={minY + p}
52
+        width={p}
53
+        height={Math.max(0, height - p * 2)}
54
+        edge={TransformEdge.Left}
55
+      />
56
+      <Corner
57
+        x={minX}
58
+        y={minY}
59
+        width={cp}
60
+        height={cp}
61
+        corner={TransformCorner.TopLeft}
62
+      />
63
+      <Corner
64
+        x={maxX}
65
+        y={minY}
66
+        width={cp}
67
+        height={cp}
68
+        corner={TransformCorner.TopRight}
69
+      />
70
+      <Corner
71
+        x={maxX}
72
+        y={maxY}
73
+        width={cp}
74
+        height={cp}
75
+        corner={TransformCorner.BottomRight}
76
+      />
77
+      <Corner
78
+        x={minX}
79
+        y={maxY}
80
+        width={cp}
81
+        height={cp}
82
+        corner={TransformCorner.BottomLeft}
83
+      />
90 84
     </g>
91 85
   )
92 86
 }
@@ -142,6 +136,7 @@ function Corner({
142 136
           rCorner.current.setPointerCapture(e.pointerId)
143 137
           state.send("POINTED_BOUNDS_CORNER", inputs.pointerDown(e, corner))
144 138
         }}
139
+        onPointerCancelCapture={() => console.log("oops")}
145 140
         onPointerUp={(e) => {
146 141
           e.stopPropagation()
147 142
           rCorner.current.releasePointerCapture(e.pointerId)

+ 21
- 9
components/canvas/shape.tsx Ver arquivo

@@ -7,6 +7,7 @@ import styled from "styles"
7 7
 function Shape({ id }: { id: string }) {
8 8
   const rGroup = useRef<SVGGElement>(null)
9 9
 
10
+  const isHovered = useSelector((state) => state.data.hoveredId === id)
10 11
   const isSelected = useSelector((state) => state.values.selectedIds.has(id))
11 12
 
12 13
   const shape = useSelector(
@@ -32,23 +33,35 @@ function Shape({ id }: { id: string }) {
32 33
   )
33 34
 
34 35
   const handlePointerEnter = useCallback(
35
-    () => state.send("HOVERED_SHAPE", { id }),
36
-    [id]
36
+    (e: React.PointerEvent) => {
37
+      state.send("HOVERED_SHAPE", inputs.pointerEnter(e, id))
38
+    },
39
+    [id, shape]
40
+  )
41
+
42
+  const handlePointerMove = useCallback(
43
+    (e: React.PointerEvent) => {
44
+      state.send("MOVED_OVER_SHAPE", inputs.pointerEnter(e, id))
45
+    },
46
+    [id, shape]
37 47
   )
38 48
 
39 49
   const handlePointerLeave = useCallback(
40
-    () => state.send("UNHOVERED_SHAPE", { id }),
50
+    () => state.send("UNHOVERED_SHAPE", { target: id }),
41 51
     [id]
42 52
   )
53
+
43 54
   return (
44 55
     <StyledGroup
45 56
       ref={rGroup}
57
+      isHovered={isHovered}
46 58
       isSelected={isSelected}
47 59
       transform={`translate(${shape.point})`}
48 60
       onPointerDown={handlePointerDown}
49 61
       onPointerUp={handlePointerUp}
50 62
       onPointerEnter={handlePointerEnter}
51 63
       onPointerLeave={handlePointerLeave}
64
+      onPointerMove={handlePointerMove}
52 65
     >
53 66
       <defs>{getShapeUtils(shape).render(shape)}</defs>
54 67
       <HoverIndicator as="use" xlinkHref={"#" + id} />
@@ -86,13 +99,12 @@ const StyledGroup = styled("g", {
86 99
         [`& ${Indicator}`]: {
87 100
           stroke: "$selected",
88 101
         },
89
-        [`&:hover ${HoverIndicator}`]: {
90
-          opacity: "1",
91
-          stroke: "$hint",
92
-        },
93 102
       },
94
-      false: {
95
-        [`&:hover ${HoverIndicator}`]: {
103
+      false: {},
104
+    },
105
+    isHovered: {
106
+      true: {
107
+        [`& ${HoverIndicator}`]: {
96 108
           opacity: "1",
97 109
           stroke: "$hint",
98 110
         },

+ 9
- 0
components/code-panel/code-as-string.ts Ver arquivo

@@ -0,0 +1,9 @@
1
+// This is the code library.
2
+
3
+export default `
4
+
5
+// Hello world
6
+const name = "steve"
7
+const age = 93
8
+
9
+`

+ 101
- 0
components/code-panel/code-docs.tsx Ver arquivo

@@ -0,0 +1,101 @@
1
+import styled from "styles"
2
+
3
+export default function CodeDocs({ isHidden }: { isHidden: boolean }) {
4
+  return (
5
+    <StyledDocs isHidden={isHidden}>
6
+      <h2>Docs</h2>
7
+    </StyledDocs>
8
+  )
9
+}
10
+
11
+const StyledDocs = styled("div", {
12
+  position: "absolute",
13
+  backgroundColor: "$panel",
14
+  top: 0,
15
+  left: 0,
16
+  width: "100%",
17
+  height: "100%",
18
+  padding: 16,
19
+  font: "$docs",
20
+  overflowY: "scroll",
21
+  userSelect: "none",
22
+  paddingBottom: 64,
23
+
24
+  variants: {
25
+    isHidden: {
26
+      true: {
27
+        visibility: "hidden",
28
+      },
29
+      false: {
30
+        visibility: "visible",
31
+      },
32
+    },
33
+  },
34
+
35
+  "& ol": {},
36
+
37
+  "& li": {
38
+    marginTop: 8,
39
+    marginBottom: 4,
40
+  },
41
+
42
+  "& code": {
43
+    font: "$mono",
44
+  },
45
+
46
+  "& hr": {
47
+    margin: "32px 0",
48
+    borderColor: "$muted",
49
+  },
50
+
51
+  "& h2": {
52
+    margin: "24px 0px",
53
+  },
54
+
55
+  "& h3": {
56
+    fontSize: 20,
57
+    margin: "48px 0px 32px 0px",
58
+  },
59
+
60
+  "& h3 > code": {
61
+    fontWeight: 600,
62
+    font: "$monoheading",
63
+  },
64
+
65
+  "& h4": {
66
+    margin: "32px 0px 0px 0px",
67
+  },
68
+
69
+  "& h4 > code": {
70
+    font: "$monoheading",
71
+    fontSize: 16,
72
+    userSelect: "all",
73
+  },
74
+
75
+  "& h4 > code > i": {
76
+    fontSize: 14,
77
+    color: "$muted",
78
+  },
79
+
80
+  "& pre": {
81
+    backgroundColor: "$bounds_bg",
82
+    padding: 16,
83
+    borderRadius: 4,
84
+    userSelect: "all",
85
+    margin: "24px 0",
86
+  },
87
+
88
+  "& p > code, blockquote > code": {
89
+    backgroundColor: "$bounds_bg",
90
+    padding: "2px 4px",
91
+    borderRadius: 2,
92
+    color: "$code",
93
+  },
94
+
95
+  "& blockquote": {
96
+    backgroundColor: "rgba(144, 144, 144, .05)",
97
+    padding: 12,
98
+    margin: "20px 0",
99
+    borderRadius: 8,
100
+  },
101
+})

+ 214
- 0
components/code-panel/code-editor.tsx Ver arquivo

@@ -0,0 +1,214 @@
1
+import Editor, { Monaco } from "@monaco-editor/react"
2
+import useTheme from "hooks/useTheme"
3
+import prettier from "prettier/standalone"
4
+import parserTypeScript from "prettier/parser-typescript"
5
+import codeAsString from "./code-as-string"
6
+import React, { useCallback, useEffect, useRef } from "react"
7
+import styled from "styles"
8
+import { IMonaco, IMonacoEditor } from "types"
9
+
10
+interface Props {
11
+  value: string
12
+  error: { line: number }
13
+  fontSize: number
14
+  monacoRef?: React.MutableRefObject<IMonaco>
15
+  editorRef?: React.MutableRefObject<IMonacoEditor>
16
+  readOnly?: boolean
17
+  onMount?: (value: string, editor: IMonacoEditor) => void
18
+  onUnmount?: (editor: IMonacoEditor) => void
19
+  onChange?: (value: string, editor: IMonacoEditor) => void
20
+  onSave?: (value: string, editor: IMonacoEditor) => void
21
+  onError?: (error: Error, line: number, col: number) => void
22
+  onKey?: () => void
23
+}
24
+
25
+export default function CodeEditor({
26
+  editorRef,
27
+  monacoRef,
28
+  fontSize,
29
+  value,
30
+  error,
31
+  readOnly,
32
+  onChange,
33
+  onSave,
34
+  onKey,
35
+}: Props) {
36
+  const { theme } = useTheme()
37
+  const rEditor = useRef<IMonacoEditor>(null)
38
+  const rMonaco = useRef<IMonaco>(null)
39
+
40
+  const handleBeforeMount = useCallback((monaco: Monaco) => {
41
+    if (monacoRef) {
42
+      monacoRef.current = monaco
43
+    }
44
+    rMonaco.current = monaco
45
+
46
+    monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
47
+      allowJs: true,
48
+      checkJs: false,
49
+      strict: false,
50
+      noLib: true,
51
+      lib: ["es6"],
52
+      target: monaco.languages.typescript.ScriptTarget.ES2015,
53
+      allowNonTsExtensions: true,
54
+    })
55
+
56
+    monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
57
+
58
+    monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true)
59
+
60
+    monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
61
+      noSemanticValidation: true,
62
+      noSyntaxValidation: true,
63
+    })
64
+
65
+    monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
66
+      noSemanticValidation: true,
67
+      noSyntaxValidation: true,
68
+    })
69
+
70
+    monaco.languages.typescript.javascriptDefaults.addExtraLib(codeAsString)
71
+
72
+    monaco.languages.registerDocumentFormattingEditProvider("javascript", {
73
+      async provideDocumentFormattingEdits(model) {
74
+        const text = prettier.format(model.getValue(), {
75
+          parser: "typescript",
76
+          plugins: [parserTypeScript],
77
+          singleQuote: true,
78
+          trailingComma: "es5",
79
+          semi: false,
80
+        })
81
+
82
+        return [
83
+          {
84
+            range: model.getFullModelRange(),
85
+            text,
86
+          },
87
+        ]
88
+      },
89
+    })
90
+  }, [])
91
+
92
+  const handleMount = useCallback((editor: IMonacoEditor) => {
93
+    if (editorRef) {
94
+      editorRef.current = editor
95
+    }
96
+    rEditor.current = editor
97
+
98
+    editor.updateOptions({
99
+      fontSize,
100
+      wordBasedSuggestions: false,
101
+      minimap: { enabled: false },
102
+      lightbulb: {
103
+        enabled: false,
104
+      },
105
+      readOnly,
106
+    })
107
+  }, [])
108
+
109
+  const handleChange = useCallback((code: string | undefined) => {
110
+    onChange(code, rEditor.current)
111
+  }, [])
112
+
113
+  const handleKeydown = useCallback(
114
+    (e: React.KeyboardEvent<HTMLDivElement>) => {
115
+      onKey && onKey()
116
+      e.stopPropagation()
117
+      const metaKey = navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey
118
+      if (e.key === "s" && metaKey) {
119
+        const editor = rEditor.current
120
+        if (!editor) return
121
+        editor
122
+          .getAction("editor.action.formatDocument")
123
+          .run()
124
+          .then(() =>
125
+            onSave(rEditor.current?.getModel().getValue(), rEditor.current)
126
+          )
127
+
128
+        e.preventDefault()
129
+      }
130
+      if (e.key === "p" && metaKey) {
131
+        e.preventDefault()
132
+      }
133
+      if (e.key === "d" && metaKey) {
134
+        e.preventDefault()
135
+      }
136
+    },
137
+    []
138
+  )
139
+
140
+  const handleKeyUp = useCallback(
141
+    (e: React.KeyboardEvent<HTMLDivElement>) => e.stopPropagation(),
142
+    []
143
+  )
144
+
145
+  const rDecorations = useRef<any>([])
146
+
147
+  useEffect(() => {
148
+    const monaco = rMonaco.current
149
+    if (!monaco) return
150
+    const editor = rEditor.current
151
+    if (!editor) return
152
+
153
+    if (!error) {
154
+      rDecorations.current = editor.deltaDecorations(rDecorations.current, [])
155
+      return
156
+    }
157
+
158
+    if (!error.line) return
159
+
160
+    rDecorations.current = editor.deltaDecorations(rDecorations.current, [
161
+      {
162
+        range: new monaco.Range(
163
+          Number(error.line) - 1,
164
+          0,
165
+          Number(error.line) - 1,
166
+          0
167
+        ),
168
+        options: {
169
+          isWholeLine: true,
170
+          className: "editorLineError",
171
+        },
172
+      },
173
+    ])
174
+  }, [error])
175
+
176
+  useEffect(() => {
177
+    const monaco = rMonaco.current
178
+    if (!monaco) return
179
+    monaco.editor.setTheme(theme === "dark" ? "vs-dark" : "light")
180
+  }, [theme])
181
+
182
+  useEffect(() => {
183
+    const editor = rEditor.current
184
+    if (!editor) return
185
+
186
+    editor.updateOptions({
187
+      fontSize,
188
+    })
189
+  }, [fontSize])
190
+
191
+  return (
192
+    <EditorContainer onKeyDown={handleKeydown} onKeyUp={handleKeyUp}>
193
+      <Editor
194
+        height="100%"
195
+        language="javascript"
196
+        value={value}
197
+        theme={theme === "dark" ? "vs-dark" : "light"}
198
+        beforeMount={handleBeforeMount}
199
+        onMount={handleMount}
200
+        onChange={handleChange}
201
+      />
202
+    </EditorContainer>
203
+  )
204
+}
205
+
206
+const EditorContainer = styled("div", {
207
+  height: "100%",
208
+  pointerEvents: "all",
209
+  userSelect: "all",
210
+
211
+  ".editorLineError": {
212
+    backgroundColor: "$lineError",
213
+  },
214
+})

+ 315
- 0
components/code-panel/code-panel.tsx Ver arquivo

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

+ 47
- 0
components/code-panel/example-code.ts Ver arquivo

@@ -0,0 +1,47 @@
1
+export default `// Basic nodes and globs
2
+
3
+const nodeA = new Node({
4
+  x: -100,
5
+  y: 0,
6
+});
7
+
8
+const nodeB = new Node({
9
+  x: 100,
10
+  y: 0,
11
+});
12
+
13
+const glob = new Glob({
14
+  start: nodeA,
15
+  end: nodeB,
16
+  D: { x: 0, y: 60 },
17
+  Dp: { x: 0, y: 90 },
18
+});
19
+
20
+// Something more interesting...
21
+
22
+const PI2 = Math.PI * 2,
23
+  center = { x: 0, y: 0 },
24
+  radius = 400;
25
+
26
+let prev;
27
+
28
+for (let i = 0; i < 21; i++) {
29
+  const t = i * (PI2 / 20);
30
+
31
+  const node = new Node({
32
+    x: center.x + radius * Math.sin(t),
33
+    y: center.y + radius * Math.cos(t),
34
+  });
35
+
36
+  if (prev !== undefined) {
37
+    new Glob({
38
+      start: prev,
39
+      end: node,
40
+      D: center,
41
+      Dp: center,
42
+    });
43
+  }
44
+
45
+  prev = node;
46
+}
47
+`

+ 2
- 0
components/editor.tsx Ver arquivo

@@ -1,6 +1,7 @@
1 1
 import useKeyboardEvents from "hooks/useKeyboardEvents"
2 2
 import Canvas from "./canvas/canvas"
3 3
 import StatusBar from "./status-bar"
4
+import CodePanel from "./code-panel/code-panel"
4 5
 
5 6
 export default function Editor() {
6 7
   useKeyboardEvents()
@@ -9,6 +10,7 @@ export default function Editor() {
9 10
     <>
10 11
       <Canvas />
11 12
       <StatusBar />
13
+      {/* <CodePanel /> */}
12 14
     </>
13 15
   )
14 16
 }

+ 12
- 0
hooks/useTheme.ts Ver arquivo

@@ -0,0 +1,12 @@
1
+import { useCallback } from "react"
2
+import state, { useSelector } from "state"
3
+
4
+export default function useTheme() {
5
+  const theme = useSelector((state) =>
6
+    state.data.settings.darkMode ? "dark" : "light"
7
+  )
8
+
9
+  const toggleTheme = useCallback(() => state.send("TOGGLED_THEME"), [])
10
+
11
+  return { theme, toggleTheme }
12
+}

+ 24
- 0
lib/code/circle.ts Ver arquivo

@@ -0,0 +1,24 @@
1
+import CodeShape from "./index"
2
+import { v4 as uuid } from "uuid"
3
+import { CircleShape, ShapeType } from "types"
4
+
5
+export default class Circle extends CodeShape<CircleShape> {
6
+  constructor(props = {} as Partial<CircleShape>) {
7
+    super({
8
+      id: uuid(),
9
+      type: ShapeType.Circle,
10
+      name: "Circle",
11
+      parentId: "page0",
12
+      childIndex: 0,
13
+      point: [0, 0],
14
+      rotation: 0,
15
+      radius: 20,
16
+      style: {},
17
+      ...props,
18
+    })
19
+  }
20
+
21
+  get radius() {
22
+    return this.shape.radius
23
+  }
24
+}

+ 54
- 0
lib/code/index.ts Ver arquivo

@@ -0,0 +1,54 @@
1
+import { Shape } from "types"
2
+import * as vec from "utils/vec"
3
+import { getShapeUtils } from "lib/shapes"
4
+
5
+export default class CodeShape<T extends Shape> {
6
+  private _shape: T
7
+
8
+  constructor(props: T) {
9
+    this._shape = props
10
+    shapeMap.add(this)
11
+  }
12
+
13
+  destroy() {
14
+    shapeMap.delete(this)
15
+  }
16
+
17
+  moveTo(point: number[]) {
18
+    this.shape.point = point
19
+  }
20
+
21
+  translate(delta: number[]) {
22
+    this.shape.point = vec.add(this._shape.point, delta)
23
+  }
24
+
25
+  rotate(rotation: number) {
26
+    this.shape.rotation = rotation
27
+  }
28
+
29
+  scale(scale: number) {
30
+    return getShapeUtils(this.shape).scale(this.shape, scale)
31
+  }
32
+
33
+  getBounds() {
34
+    return getShapeUtils(this.shape).getBounds(this.shape)
35
+  }
36
+
37
+  hitTest(point: number[]) {
38
+    return getShapeUtils(this.shape).hitTest(this.shape, point)
39
+  }
40
+
41
+  get shape() {
42
+    return this._shape
43
+  }
44
+
45
+  get point() {
46
+    return [...this.shape.point]
47
+  }
48
+
49
+  get rotation() {
50
+    return this.shape.rotation
51
+  }
52
+}
53
+
54
+export const shapeMap = new Set<CodeShape<Shape>>([])

+ 24
- 0
lib/code/rectangle.ts Ver arquivo

@@ -0,0 +1,24 @@
1
+import CodeShape from "./index"
2
+import { v4 as uuid } from "uuid"
3
+import { RectangleShape, ShapeType } from "types"
4
+
5
+export default class Rectangle extends CodeShape<RectangleShape> {
6
+  constructor(props = {} as Partial<RectangleShape>) {
7
+    super({
8
+      id: uuid(),
9
+      type: ShapeType.Rectangle,
10
+      name: "Rectangle",
11
+      parentId: "page0",
12
+      childIndex: 0,
13
+      point: [0, 0],
14
+      size: [1, 1],
15
+      rotation: 0,
16
+      style: {},
17
+      ...props,
18
+    })
19
+  }
20
+
21
+  get size() {
22
+    return this.shape.size
23
+  }
24
+}

+ 7
- 4
lib/shapes/circle.tsx Ver arquivo

@@ -4,6 +4,7 @@ import { CircleShape, ShapeType } from "types"
4 4
 import { createShape } from "./index"
5 5
 import { boundsContained } from "utils/bounds"
6 6
 import { intersectCircleBounds } from "utils/intersections"
7
+import { pointInCircle } from "utils/hitTests"
7 8
 
8 9
 const circle = createShape<CircleShape>({
9 10
   boundsCache: new WeakMap([]),
@@ -16,8 +17,8 @@ const circle = createShape<CircleShape>({
16 17
       parentId: "page0",
17 18
       childIndex: 0,
18 19
       point: [0, 0],
19
-      radius: 20,
20 20
       rotation: 0,
21
+      radius: 20,
21 22
       style: {},
22 23
       ...props,
23 24
     }
@@ -51,9 +52,11 @@ const circle = createShape<CircleShape>({
51 52
     return bounds
52 53
   },
53 54
 
54
-  hitTest(shape, test) {
55
-    return (
56
-      vec.dist(vec.addScalar(shape.point, shape.radius), test) < shape.radius
55
+  hitTest(shape, point) {
56
+    return pointInCircle(
57
+      point,
58
+      vec.addScalar(shape.point, shape.radius),
59
+      shape.radius
57 60
     )
58 61
   },
59 62
 

+ 1
- 1
lib/shapes/dot.tsx Ver arquivo

@@ -50,7 +50,7 @@ const dot = createShape<DotShape>({
50 50
   },
51 51
 
52 52
   hitTest(shape, test) {
53
-    return vec.dist(shape.point, test) < 4
53
+    return true
54 54
   },
55 55
 
56 56
   hitTestBounds(this, shape, brushBounds) {

+ 6
- 1
lib/shapes/ellipse.tsx Ver arquivo

@@ -57,7 +57,12 @@ const ellipse = createShape<EllipseShape>({
57 57
   },
58 58
 
59 59
   hitTest(shape, point) {
60
-    return pointInEllipse(point, shape.point, shape.radiusX, shape.radiusY)
60
+    return pointInEllipse(
61
+      point,
62
+      vec.add(shape.point, [shape.radiusX, shape.radiusY]),
63
+      shape.radiusX,
64
+      shape.radiusY
65
+    )
61 66
   },
62 67
 
63 68
   hitTestBounds(this, shape, brushBounds) {

+ 1
- 1
lib/shapes/line.tsx Ver arquivo

@@ -59,7 +59,7 @@ const line = createShape<LineShape>({
59 59
   },
60 60
 
61 61
   hitTest(shape, test) {
62
-    return vec.dist(shape.point, test) < 4
62
+    return true
63 63
   },
64 64
 
65 65
   hitTestBounds(this, shape, brushBounds) {

+ 13
- 2
lib/shapes/polyline.tsx Ver arquivo

@@ -57,8 +57,19 @@ const polyline = createShape<PolylineShape>({
57 57
     return bounds
58 58
   },
59 59
 
60
-  hitTest(shape) {
61
-    return true
60
+  hitTest(shape, point) {
61
+    let pt = vec.sub(point, shape.point)
62
+    let prev = shape.points[0]
63
+
64
+    for (let i = 1; i < shape.points.length; i++) {
65
+      let curr = shape.points[i]
66
+      if (vec.distanceToLineSegment(prev, curr, pt) < 4) {
67
+        return true
68
+      }
69
+      prev = curr
70
+    }
71
+
72
+    return false
62 73
   },
63 74
 
64 75
   hitTestBounds(this, shape, bounds) {

+ 4
- 0
package.json Ver arquivo

@@ -8,20 +8,24 @@
8 8
     "start": "next start"
9 9
   },
10 10
   "dependencies": {
11
+    "@monaco-editor/react": "^4.1.3",
11 12
     "@state-designer/react": "^1.7.1",
12 13
     "@stitches/react": "^0.1.9",
13 14
     "@types/uuid": "^8.3.0",
14 15
     "framer-motion": "^4.1.16",
15 16
     "next": "10.2.0",
16 17
     "perfect-freehand": "^0.4.7",
18
+    "prettier": "^2.3.0",
17 19
     "react": "17.0.2",
18 20
     "react-dom": "17.0.2",
21
+    "react-feather": "^2.0.9",
19 22
     "uuid": "^8.3.2"
20 23
   },
21 24
   "devDependencies": {
22 25
     "@types/next": "^9.0.0",
23 26
     "@types/react": "^17.0.5",
24 27
     "@types/react-dom": "^17.0.3",
28
+    "monaco-editor": "^0.24.0",
25 29
     "typescript": "^4.2.4"
26 30
   }
27 31
 }

+ 7
- 0
state/data.ts Ver arquivo

@@ -90,4 +90,11 @@ export const defaultDocument: Data["document"] = {
90 90
       },
91 91
     },
92 92
   },
93
+  code: {
94
+    file0: {
95
+      id: "file0",
96
+      name: "index.ts",
97
+      code: "// Hello world",
98
+    },
99
+  },
93 100
 }

+ 17
- 0
state/inputs.tsx Ver arquivo

@@ -23,6 +23,23 @@ class Inputs {
23 23
     return info
24 24
   }
25 25
 
26
+  pointerEnter(e: PointerEvent | React.PointerEvent, target: string) {
27
+    const { shiftKey, ctrlKey, metaKey, altKey } = e
28
+
29
+    const info = {
30
+      target,
31
+      pointerId: e.pointerId,
32
+      origin: [e.clientX, e.clientY],
33
+      point: [e.clientX, e.clientY],
34
+      shiftKey,
35
+      ctrlKey,
36
+      metaKey: isDarwin() ? metaKey : ctrlKey,
37
+      altKey,
38
+    }
39
+
40
+    return info
41
+  }
42
+
26 43
   pointerMove(e: PointerEvent | React.PointerEvent) {
27 44
     const { shiftKey, ctrlKey, metaKey, altKey } = e
28 45
 

+ 36
- 0
state/state.ts Ver arquivo

@@ -9,12 +9,17 @@ import * as Sessions from "./sessions"
9 9
 
10 10
 const initialData: Data = {
11 11
   isReadOnly: false,
12
+  settings: {
13
+    fontSize: 13,
14
+    darkMode: false,
15
+  },
12 16
   camera: {
13 17
     point: [0, 0],
14 18
     zoom: 1,
15 19
   },
16 20
   brush: undefined,
17 21
   pointedId: null,
22
+  hoveredId: null,
18 23
   selectedIds: new Set([]),
19 24
   currentPageId: "page0",
20 25
   document: defaultDocument,
@@ -45,6 +50,15 @@ const state = createState({
45 50
             POINTED_BOUNDS: { to: "pointingBounds" },
46 51
             POINTED_BOUNDS_EDGE: { to: "transformingSelection" },
47 52
             POINTED_BOUNDS_CORNER: { to: "transformingSelection" },
53
+            MOVED_OVER_SHAPE: {
54
+              if: "pointHitsShape",
55
+              then: {
56
+                unless: "shapeIsHovered",
57
+                do: "setHoveredId",
58
+              },
59
+              else: { if: "shapeIsHovered", do: "clearHoveredId" },
60
+            },
61
+            UNHOVERED_SHAPE: "clearHoveredId",
48 62
             POINTED_SHAPE: [
49 63
               "setPointedId",
50 64
               {
@@ -135,6 +149,22 @@ const state = createState({
135 149
     isPressingShiftKey(data, payload: { shiftKey: boolean }) {
136 150
       return payload.shiftKey
137 151
     },
152
+    shapeIsHovered(data, payload: { target: string }) {
153
+      return data.hoveredId === payload.target
154
+    },
155
+    pointHitsShape(data, payload: { target: string; point: number[] }) {
156
+      const shape =
157
+        data.document.pages[data.currentPageId].shapes[payload.target]
158
+
159
+      console.log(
160
+        getShapeUtils(shape).hitTest(shape, screenToWorld(payload.point, data))
161
+      )
162
+
163
+      return getShapeUtils(shape).hitTest(
164
+        shape,
165
+        screenToWorld(payload.point, data)
166
+      )
167
+    },
138 168
   },
139 169
   actions: {
140 170
     // History
@@ -199,6 +229,12 @@ const state = createState({
199 229
     },
200 230
 
201 231
     // Selection
232
+    setHoveredId(data, payload: PointerInfo) {
233
+      data.hoveredId = payload.target
234
+    },
235
+    clearHoveredId(data) {
236
+      data.hoveredId = undefined
237
+    },
202 238
     setPointedId(data, payload: PointerInfo) {
203 239
       data.pointedId = payload.target
204 240
     },

+ 4
- 1
styles/stitches.config.ts Ver arquivo

@@ -12,6 +12,9 @@ const { styled, global, css, theme, getCssString } = createCss({
12 12
       selected: "rgba(66, 133, 244, 1.000)",
13 13
       bounds: "rgba(65, 132, 244, 1.000)",
14 14
       boundsBg: "rgba(65, 132, 244, 0.100)",
15
+      border: "#aaa",
16
+      panel: "#fefefe",
17
+      text: "#000",
15 18
     },
16 19
     space: {},
17 20
     fontSizes: {
@@ -22,7 +25,7 @@ const { styled, global, css, theme, getCssString } = createCss({
22 25
       4: "18px",
23 26
     },
24 27
     fonts: {
25
-      ui: `"Recursive", system-ui, sans-serif`,
28
+      ui: '"Recursive", system-ui, sans-serif',
26 29
     },
27 30
     fontWeights: {},
28 31
     lineHeights: {},

+ 18
- 0
types.ts Ver arquivo

@@ -1,7 +1,13 @@
1
+import * as monaco from "monaco-editor/esm/vs/editor/editor.api"
2
+
1 3
 import React from "react"
2 4
 
3 5
 export interface Data {
4 6
   isReadOnly: boolean
7
+  settings: {
8
+    fontSize: number
9
+    darkMode: boolean
10
+  }
5 11
   camera: {
6 12
     point: number[]
7 13
     zoom: number
@@ -10,11 +16,19 @@ export interface Data {
10 16
   currentPageId: string
11 17
   selectedIds: Set<string>
12 18
   pointedId?: string
19
+  hoveredId?: string
13 20
   document: {
14 21
     pages: Record<string, Page>
22
+    code: Record<string, CodeFile>
15 23
   }
16 24
 }
17 25
 
26
+export interface CodeFile {
27
+  id: string
28
+  name: string
29
+  code: string
30
+}
31
+
18 32
 export interface Page {
19 33
   id: string
20 34
   type: "page"
@@ -172,3 +186,7 @@ export enum TransformCorner {
172 186
   BottomRight = "bottom_right_corner",
173 187
   BottomLeft = "bottom_left_corner",
174 188
 }
189
+
190
+export type IMonaco = typeof monaco
191
+
192
+export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor

+ 38
- 0
yarn.lock Ver arquivo

@@ -1155,6 +1155,22 @@
1155 1155
     "@types/yargs" "^15.0.0"
1156 1156
     chalk "^3.0.0"
1157 1157
 
1158
+"@monaco-editor/loader@^1.0.1":
1159
+  version "1.0.1"
1160
+  resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.0.1.tgz#7068c9b07bbc65387c0e7a4df6dac0a326155905"
1161
+  integrity sha512-hycGOhLqLYjnD0A/FHs56covEQWnDFrSnm/qLKkB/yoeayQ7ju+Vaj4SdTojGrXeY6jhMDx59map0+Jqwquh1Q==
1162
+  dependencies:
1163
+    state-local "^1.0.6"
1164
+
1165
+"@monaco-editor/react@^4.1.3":
1166
+  version "4.1.3"
1167
+  resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.1.3.tgz#7dcaa584f2a4e8bd8f5298604f0b5368f8ebca55"
1168
+  integrity sha512-kqcjVuoy6btcgALAk4RV/SlasveM+WTw5lzzlyq5FhKXjF8wu5tSe/2oCQ1uhLpcdtxcHfx3L0HrcAPWnejFnQ==
1169
+  dependencies:
1170
+    "@monaco-editor/loader" "^1.0.1"
1171
+    prop-types "^15.7.2"
1172
+    state-local "^1.0.7"
1173
+
1158 1174
 "@next/env@10.2.0":
1159 1175
   version "10.2.0"
1160 1176
   resolved "https://registry.yarnpkg.com/@next/env/-/env-10.2.0.tgz#154dbce2efa3ad067ebd20b7d0aa9aed775e7c97"
@@ -5041,6 +5057,11 @@ mkdirp@0.x, mkdirp@^0.5.1:
5041 5057
   dependencies:
5042 5058
     minimist "^1.2.5"
5043 5059
 
5060
+monaco-editor@^0.24.0:
5061
+  version "0.24.0"
5062
+  resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.24.0.tgz#990b55096bcc95d08d8d28e55264c6eb17707269"
5063
+  integrity sha512-o1f0Lz6ABFNTtnEqqqvlY9qzNx24rQZx1RgYNQ8SkWkE+Ka63keHH/RqxQ4QhN4fs/UYOnvAtEUZsPrzccH++A==
5064
+
5044 5065
 mri@^1.1.0:
5045 5066
   version "1.1.6"
5046 5067
   resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6"
@@ -5700,6 +5721,11 @@ prettier@^1.19.1:
5700 5721
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
5701 5722
   integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
5702 5723
 
5724
+prettier@^2.3.0:
5725
+  version "2.3.0"
5726
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
5727
+  integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
5728
+
5703 5729
 pretty-format@^25.2.1, pretty-format@^25.5.0:
5704 5730
   version "25.5.0"
5705 5731
   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a"
@@ -5846,6 +5872,13 @@ react-dom@17.0.2:
5846 5872
     object-assign "^4.1.1"
5847 5873
     scheduler "^0.20.2"
5848 5874
 
5875
+react-feather@^2.0.9:
5876
+  version "2.0.9"
5877
+  resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.9.tgz#6e42072130d2fa9a09d4476b0e61b0ed17814480"
5878
+  integrity sha512-yMfCGRkZdXwIs23Zw/zIWCJO3m3tlaUvtHiXlW+3FH7cIT6fiK1iJ7RJWugXq7Fso8ZaQyUm92/GOOHXvkiVUw==
5879
+  dependencies:
5880
+    prop-types "^15.7.2"
5881
+
5849 5882
 react-is@16.13.1, react-is@^16.12.0, react-is@^16.8.1:
5850 5883
   version "16.13.1"
5851 5884
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -6591,6 +6624,11 @@ stacktrace-parser@0.1.10:
6591 6624
   dependencies:
6592 6625
     type-fest "^0.7.1"
6593 6626
 
6627
+state-local@^1.0.6, state-local@^1.0.7:
6628
+  version "1.0.7"
6629
+  resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
6630
+  integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
6631
+
6594 6632
 static-extend@^0.1.1:
6595 6633
   version "0.1.2"
6596 6634
   resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"

Carregando…
Cancelar
Salvar