浏览代码

perf improvements around selected / hovered shapes

main
Steve Ruiz 3 年前
父节点
当前提交
8ff8b87a9e

+ 7
- 12
components/canvas/bounds/bounding-box.tsx 查看文件

@@ -2,11 +2,9 @@ import * as React from 'react'
2 2
 import { Edge, Corner } from 'types'
3 3
 import { useSelector } from 'state'
4 4
 import {
5
-  deepCompareArrays,
6 5
   getBoundsCenter,
7 6
   getCurrentCamera,
8 7
   getPage,
9
-  getSelectedIds,
10 8
   getSelectedShapes,
11 9
   isMobile,
12 10
 } from 'utils'
@@ -24,25 +22,22 @@ export default function Bounds(): JSX.Element {
24 22
 
25 23
   const bounds = useSelector((s) => s.values.selectedBounds)
26 24
 
27
-  const selectedIds = useSelector(
28
-    (s) => Array.from(s.values.selectedIds.values()),
29
-    deepCompareArrays
30
-  )
31
-
32
-  const rotation = useSelector(({ data }) =>
33
-    getSelectedIds(data).size === 1 ? getSelectedShapes(data)[0].rotation : 0
25
+  const rotation = useSelector((s) =>
26
+    s.values.selectedIds.length === 1
27
+      ? getSelectedShapes(s.data)[0].rotation
28
+      : 0
34 29
   )
35 30
 
36 31
   const isAllLocked = useSelector((s) => {
37 32
     const page = getPage(s.data)
38
-    return selectedIds.every((id) => page.shapes[id]?.isLocked)
33
+    return s.values.selectedIds.every((id) => page.shapes[id]?.isLocked)
39 34
   })
40 35
 
41 36
   const isSingleHandles = useSelector((s) => {
42 37
     const page = getPage(s.data)
43 38
     return (
44
-      selectedIds.length === 1 &&
45
-      page.shapes[selectedIds[0]]?.handles !== undefined
39
+      s.values.selectedIds.length === 1 &&
40
+      page.shapes[s.values.selectedIds[0]]?.handles !== undefined
46 41
     )
47 42
   })
48 43
 

+ 17
- 15
components/canvas/bounds/bounds-bg.tsx 查看文件

@@ -2,7 +2,7 @@ import { useRef } from 'react'
2 2
 import state, { useSelector } from 'state'
3 3
 import inputs from 'state/inputs'
4 4
 import styled from 'styles'
5
-import { deepCompareArrays, getPage } from 'utils'
5
+import { getPage } from 'utils'
6 6
 
7 7
 function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
8 8
   if (!inputs.canAccept(e.pointerId)) return
@@ -31,28 +31,30 @@ export default function BoundsBg(): JSX.Element {
31 31
 
32 32
   const isSelecting = useSelector((s) => s.isIn('selecting'))
33 33
 
34
-  const selectedIds = useSelector(
35
-    (s) => Array.from(s.values.selectedIds.values()),
36
-    deepCompareArrays
37
-  )
38
-
39 34
   const rotation = useSelector((s) => {
35
+    const selectedIds = s.values.selectedIds
36
+
40 37
     if (selectedIds.length === 1) {
41
-      const { shapes } = getPage(s.data)
42
-      const selected = Array.from(s.values.selectedIds.values())[0]
43
-      return shapes[selected]?.rotation
38
+      const selected = selectedIds[0]
39
+      const page = getPage(s.data)
40
+
41
+      return page.shapes[selected]?.rotation
44 42
     } else {
45 43
       return 0
46 44
     }
47 45
   })
48 46
 
49 47
   const isAllHandles = useSelector((s) => {
50
-    const page = getPage(s.data)
51
-    const selectedIds = Array.from(s.values.selectedIds.values())
52
-    return (
53
-      selectedIds.length === 1 &&
54
-      page.shapes[selectedIds[0]]?.handles !== undefined
55
-    )
48
+    const selectedIds = s.values.selectedIds
49
+
50
+    if (selectedIds.length === 1) {
51
+      const page = getPage(s.data)
52
+      const selected = selectedIds[0]
53
+
54
+      return (
55
+        selectedIds.length === 1 && page.shapes[selected]?.handles !== undefined
56
+      )
57
+    }
56 58
   })
57 59
 
58 60
   if (isAllHandles) return null

+ 4
- 8
components/canvas/bounds/handles.tsx 查看文件

@@ -3,18 +3,14 @@ import { getShapeUtils } from 'state/shape-utils'
3 3
 import { useRef } from 'react'
4 4
 import { useSelector } from 'state'
5 5
 import styled from 'styles'
6
-import { deepCompareArrays, getPage } from 'utils'
6
+import { getPage } from 'utils'
7 7
 import vec from 'utils/vec'
8 8
 
9 9
 export default function Handles(): JSX.Element {
10
-  const selectedIds = useSelector(
11
-    (s) => Array.from(s.values.selectedIds.values()),
12
-    deepCompareArrays
13
-  )
14
-
15 10
   const shape = useSelector(
16
-    ({ data }) =>
17
-      selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]]
11
+    (s) =>
12
+      s.values.selectedIds.length === 1 &&
13
+      getPage(s.data).shapes[s.values.selectedIds[0]]
18 14
   )
19 15
 
20 16
   const isSelecting = useSelector((s) =>

+ 5
- 6
components/canvas/canvas.tsx 查看文件

@@ -13,6 +13,10 @@ import Handles from './bounds/handles'
13 13
 import useCanvasEvents from 'hooks/useCanvasEvents'
14 14
 import ContextMenu from './context-menu/context-menu'
15 15
 
16
+function resetError() {
17
+  null
18
+}
19
+
16 20
 export default function Canvas(): JSX.Element {
17 21
   const rCanvas = useRef<SVGSVGElement>(null)
18 22
   const rGroup = useRef<SVGGElement>(null)
@@ -28,12 +32,7 @@ export default function Canvas(): JSX.Element {
28 32
   return (
29 33
     <ContextMenu>
30 34
       <MainSVG ref={rCanvas} {...events}>
31
-        <ErrorBoundary
32
-          FallbackComponent={ErrorFallback}
33
-          onReset={() => {
34
-            // reset the state of your app so the error doesn't happen again
35
-          }}
36
-        >
35
+        <ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
37 36
           <Defs />
38 37
           {isReady && (
39 38
             <g ref={rGroup} id="shapes">

+ 2
- 9
components/canvas/context-menu/context-menu.tsx 查看文件

@@ -5,14 +5,7 @@ import {
5 5
   IconButton as _IconButton,
6 6
   RowButton,
7 7
 } from 'components/shared'
8
-import {
9
-  commandKey,
10
-  deepCompareArrays,
11
-  getSelectedIds,
12
-  getShape,
13
-  isMobile,
14
-  setToArray,
15
-} from 'utils'
8
+import { commandKey, deepCompareArrays, getShape, isMobile } from 'utils'
16 9
 import state, { useSelector } from 'state'
17 10
 import {
18 11
   AlignType,
@@ -82,7 +75,7 @@ export default function ContextMenu({
82 75
   children: React.ReactNode
83 76
 }): JSX.Element {
84 77
   const selectedShapeIds = useSelector(
85
-    (s) => setToArray(getSelectedIds(s.data)),
78
+    (s) => s.values.selectedIds,
86 79
     deepCompareArrays
87 80
   )
88 81
 

+ 13
- 9
components/canvas/defs.tsx 查看文件

@@ -1,6 +1,6 @@
1 1
 import { getShapeStyle } from 'state/shape-styles'
2 2
 import { getShapeUtils } from 'state/shape-utils'
3
-import React, { memo } from 'react'
3
+import React from 'react'
4 4
 import { useSelector } from 'state'
5 5
 import { getCurrentCamera } from 'utils'
6 6
 import { DotCircle, Handle } from './misc'
@@ -12,28 +12,32 @@ export default function Defs(): JSX.Element {
12 12
 
13 13
   return (
14 14
     <defs>
15
-      {shapeIdsToRender.map((id) => (
16
-        <Def key={id} id={id} />
17
-      ))}
18 15
       <DotCircle id="dot" r={4} />
19 16
       <Handle id="handle" r={4} />
20 17
       <ExpandDef />
18
+      {shapeIdsToRender.map((id) => (
19
+        <Def key={id} id={id} />
20
+      ))}
21 21
     </defs>
22 22
   )
23 23
 }
24 24
 
25
-const Def = memo(function Def({ id }: { id: string }) {
25
+function Def({ id }: { id: string }) {
26 26
   const shape = useShapeDef(id)
27 27
 
28 28
   if (!shape) return null
29 29
 
30 30
   const style = getShapeStyle(shape.style)
31 31
 
32
-  return React.cloneElement(
33
-    getShapeUtils(shape).render(shape, { isEditing: false }),
34
-    { id, ...style }
32
+  return (
33
+    <>
34
+      {React.cloneElement(
35
+        getShapeUtils(shape).render(shape, { isEditing: false }),
36
+        { id, ...style, strokeWidth: undefined }
37
+      )}
38
+    </>
35 39
   )
36
-})
40
+}
37 41
 
38 42
 function ExpandDef() {
39 43
   const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)

+ 43
- 0
components/canvas/hovered-shape.tsx 查看文件

@@ -0,0 +1,43 @@
1
+import { memo } from 'react'
2
+import { getShape } from 'utils'
3
+import { getShapeUtils } from 'state/shape-utils'
4
+import vec from 'utils/vec'
5
+import styled from 'styles'
6
+import { useSelector } from 'state'
7
+import { getShapeStyle } from 'state/shape-styles'
8
+
9
+function HoveredShape({ id }: { id: string }) {
10
+  const transform = useSelector((s) => {
11
+    const shape = getShape(s.data, id)
12
+    const center = getShapeUtils(shape).getCenter(shape)
13
+    const rotation = shape.rotation * (180 / Math.PI)
14
+    const parentPoint = getShape(s.data, shape.parentId)?.point || [0, 0]
15
+
16
+    return `
17
+      translate(${vec.neg(parentPoint)})
18
+      rotate(${rotation}, ${center})
19
+      translate(${shape.point})
20
+  `
21
+  })
22
+
23
+  const strokeWidth = useSelector((s) => {
24
+    const shape = getShape(s.data, id)
25
+    const style = getShapeStyle(shape.style)
26
+    return +style.strokeWidth
27
+  })
28
+
29
+  return (
30
+    <g transform={transform}>
31
+      <StyledHoverShape href={'#' + id} strokeWidth={strokeWidth + 8} />
32
+      <text>hello</text>
33
+    </g>
34
+  )
35
+}
36
+
37
+const StyledHoverShape = styled('use', {
38
+  stroke: '$selected',
39
+  filter: 'url(#expand)',
40
+  opacity: 0.1,
41
+})
42
+
43
+export default memo(HoveredShape)

+ 12
- 11
components/canvas/page.tsx 查看文件

@@ -1,5 +1,6 @@
1 1
 import { useSelector } from 'state'
2 2
 import Shape from './shape'
3
+import HoveredShape from './hovered-shape'
3 4
 import usePageShapes from 'hooks/usePageShapes'
4 5
 
5 6
 /* 
@@ -8,22 +9,22 @@ on the current page. Kind of expensive but only happens
8 9
 here; and still cheaper than any other pattern I've found.
9 10
 */
10 11
 
11
-const noOffset = [0, 0]
12
-
13 12
 export default function Page(): JSX.Element {
14
-  const currentPageShapeIds = usePageShapes()
15
-
16 13
   const isSelecting = useSelector((s) => s.isIn('selecting'))
17 14
 
15
+  const visiblePageShapeIds = usePageShapes()
16
+
17
+  const hoveredShapeId = useSelector((s) => {
18
+    return visiblePageShapeIds.find((id) => id === s.data.hoveredId)
19
+  })
20
+
18 21
   return (
19 22
     <g pointerEvents={isSelecting ? 'all' : 'none'}>
20
-      {currentPageShapeIds.map((shapeId) => (
21
-        <Shape
22
-          key={shapeId}
23
-          id={shapeId}
24
-          isSelecting={isSelecting}
25
-          parentPoint={noOffset}
26
-        />
23
+      {isSelecting && hoveredShapeId && (
24
+        <HoveredShape key={hoveredShapeId} id={hoveredShapeId} />
25
+      )}
26
+      {visiblePageShapeIds.map((id) => (
27
+        <Shape key={id} id={id} isSelecting={isSelecting} />
27 28
       ))}
28 29
     </g>
29 30
   )

+ 2
- 2
components/canvas/selected.tsx 查看文件

@@ -1,12 +1,12 @@
1 1
 import styled from 'styles'
2 2
 import { useSelector } from 'state'
3
-import { deepCompareArrays, getPage, getSelectedIds, setToArray } from 'utils'
3
+import { deepCompareArrays, getPage } from 'utils'
4 4
 import { getShapeUtils } from 'state/shape-utils'
5 5
 import { memo } from 'react'
6 6
 
7 7
 export default function Selected(): JSX.Element {
8 8
   const currentSelectedShapeIds = useSelector(
9
-    ({ data }) => setToArray(getSelectedIds(data)),
9
+    (s) => s.values.selectedIds,
10 10
     deepCompareArrays
11 11
   )
12 12
 

+ 123
- 159
components/canvas/shape.tsx 查看文件

@@ -2,108 +2,89 @@ import React, { useRef, memo, useEffect } from 'react'
2 2
 import { useSelector } from 'state'
3 3
 import styled from 'styles'
4 4
 import { getShapeUtils } from 'state/shape-utils'
5
-import { getPage, getSelectedIds, isMobile } from 'utils'
5
+import { deepCompareArrays, getPage, getShape } from 'utils'
6 6
 import useShapeEvents from 'hooks/useShapeEvents'
7
-import { Shape as _Shape } from 'types'
8 7
 import vec from 'utils/vec'
9 8
 import { getShapeStyle } from 'state/shape-styles'
10
-
11
-const isMobileDevice = isMobile()
9
+import useShapeDef from 'hooks/useShape'
12 10
 
13 11
 interface ShapeProps {
14 12
   id: string
15 13
   isSelecting: boolean
16
-  parentPoint: number[]
17 14
 }
18 15
 
19
-function Shape({ id, isSelecting, parentPoint }: ShapeProps): JSX.Element {
16
+function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
20 17
   const rGroup = useRef<SVGGElement>(null)
21
-  const rFocusable = useRef<HTMLTextAreaElement>(null)
22
-
23
-  const isEditing = useSelector((s) => s.data.editingId === id)
24
-
25
-  const isSelected = useSelector((s) => getSelectedIds(s.data).has(id))
26
-
27
-  const shape = useSelector((s) => getPage(s.data).shapes[id])
28
-
29
-  const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup)
30
-
31
-  useEffect(() => {
32
-    if (isEditing) {
33
-      setTimeout(() => {
34
-        const elm = rFocusable.current
35
-        if (!elm) return
36
-        elm.focus()
37
-      }, 0)
38
-    }
39
-  }, [isEditing])
40
-
41
-  // This is a problem with deleted shapes. The hooks in this component
42
-  // may sometimes run before the hook in the Page component, which means
43
-  // a deleted shape will still be pulled here before the page component
44
-  // detects the change and pulls this component.
45
-  if (!shape) return null
46 18
 
47
-  const style = getShapeStyle(shape.style)
48
-  const shapeUtils = getShapeUtils(shape)
49
-
50
-  const { isShy, isParent, isForeignObject } = shapeUtils
51
-
52
-  const bounds = shapeUtils.getBounds(shape)
53
-  const center = shapeUtils.getCenter(shape)
54
-  const rotation = shape.rotation * (180 / Math.PI)
55
-
56
-  const transform = `
57
-    translate(${vec.neg(parentPoint)})
58
-    rotate(${rotation}, ${center})
59
-    translate(${shape.point})
19
+  const shapeUtils = useSelector((s) => {
20
+    const shape = getShape(s.data, id)
21
+    return getShapeUtils(shape)
22
+  })
23
+
24
+  const isHidden = useSelector((s) => {
25
+    const shape = getShape(s.data, id)
26
+    return shape.isHidden
27
+  })
28
+
29
+  const children = useSelector((s) => {
30
+    const shape = getShape(s.data, id)
31
+    return shape.children
32
+  }, deepCompareArrays)
33
+
34
+  const isParent = shapeUtils.isParent
35
+
36
+  const isForeignObject = shapeUtils.isForeignObject
37
+
38
+  const strokeWidth = useSelector((s) => {
39
+    const shape = getShape(s.data, id)
40
+    const style = getShapeStyle(shape.style)
41
+    return +style.strokeWidth
42
+  })
43
+
44
+  const transform = useSelector((s) => {
45
+    const shape = getShape(s.data, id)
46
+    const center = shapeUtils.getCenter(shape)
47
+    const rotation = shape.rotation * (180 / Math.PI)
48
+    const parentPoint = getShape(s.data, shape.parentId)?.point || [0, 0]
49
+
50
+    return `
51
+      translate(${vec.neg(parentPoint)})
52
+      rotate(${rotation}, ${center})
53
+      translate(${shape.point})
60 54
   `
55
+  })
56
+
57
+  const events = useShapeEvents(id, isParent, rGroup)
61 58
 
62 59
   return (
63 60
     <StyledGroup
64 61
       id={id + '-group'}
65 62
       ref={rGroup}
66 63
       transform={transform}
67
-      isSelected={isSelected}
68
-      device={isMobileDevice ? 'mobile' : 'desktop'}
69 64
       {...events}
70 65
     >
71
-      {isSelecting && !isShy && (
72
-        <>
73
-          {isForeignObject ? (
74
-            <HoverIndicator
75
-              as="rect"
76
-              width={bounds.width}
77
-              height={bounds.height}
78
-              strokeWidth={1.5}
79
-              variant={'ghost'}
80
-            />
81
-          ) : (
82
-            <HoverIndicator
83
-              as="use"
84
-              href={'#' + id}
85
-              strokeWidth={+style.strokeWidth + 5}
86
-              variant={shapeUtils.canStyleFill ? 'filled' : 'hollow'}
87
-            />
88
-          )}
89
-        </>
90
-      )}
91
-
92
-      {!shape.isHidden &&
66
+      {isSelecting &&
67
+        (isForeignObject ? (
68
+          <ForeignObjectHover id={id} />
69
+        ) : (
70
+          <EventSoak
71
+            as="use"
72
+            href={'#' + id}
73
+            strokeWidth={strokeWidth + 8}
74
+            variant={shapeUtils.canStyleFill ? 'filled' : 'hollow'}
75
+          />
76
+        ))}
77
+
78
+      {!isHidden &&
93 79
         (isForeignObject ? (
94
-          shapeUtils.render(shape, { isEditing, ref: rFocusable })
80
+          <ForeignObjectRender id={id} />
95 81
         ) : (
96
-          <RealShape id={id} isParent={isParent} shape={shape} />
82
+          <RealShape id={id} isParent={isParent} strokeWidth={strokeWidth} />
97 83
         ))}
98 84
 
99 85
       {isParent &&
100
-        shape.children.map((shapeId) => (
101
-          <Shape
102
-            key={shapeId}
103
-            id={shapeId}
104
-            isSelecting={isSelecting}
105
-            parentPoint={shape.point}
106
-          />
86
+        children.map((shapeId) => (
87
+          <Shape key={shapeId} id={shapeId} isSelecting={isSelecting} />
107 88
         ))}
108 89
     </StyledGroup>
109 90
   )
@@ -111,12 +92,72 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps): JSX.Element {
111 92
 
112 93
 interface RealShapeProps {
113 94
   id: string
114
-  shape: _Shape
115 95
   isParent: boolean
96
+  strokeWidth: number
116 97
 }
117 98
 
118
-const RealShape = memo(function RealShape({ id, isParent }: RealShapeProps) {
119
-  return <StyledShape as="use" data-shy={isParent} href={'#' + id} />
99
+const RealShape = memo(function RealShape({
100
+  id,
101
+  isParent,
102
+  strokeWidth,
103
+}: RealShapeProps) {
104
+  return (
105
+    <StyledShape
106
+      as="use"
107
+      data-shy={isParent}
108
+      href={'#' + id}
109
+      strokeWidth={strokeWidth}
110
+    />
111
+  )
112
+})
113
+
114
+const ForeignObjectHover = memo(function ForeignObjectHover({
115
+  id,
116
+}: {
117
+  id: string
118
+}) {
119
+  const size = useSelector((s) => {
120
+    const shape = getPage(s.data).shapes[id]
121
+    const bounds = getShapeUtils(shape).getBounds(shape)
122
+
123
+    return [bounds.width, bounds.height]
124
+  }, deepCompareArrays)
125
+
126
+  return (
127
+    <EventSoak
128
+      as="rect"
129
+      width={size[0]}
130
+      height={size[1]}
131
+      strokeWidth={1.5}
132
+      variant={'ghost'}
133
+    />
134
+  )
135
+})
136
+
137
+const ForeignObjectRender = memo(function ForeignObjectRender({
138
+  id,
139
+}: {
140
+  id: string
141
+}) {
142
+  const shape = useShapeDef(id)
143
+
144
+  const rFocusable = useRef<HTMLTextAreaElement>(null)
145
+
146
+  const isEditing = useSelector((s) => s.data.editingId === id)
147
+
148
+  const shapeUtils = getShapeUtils(shape)
149
+
150
+  useEffect(() => {
151
+    if (isEditing) {
152
+      setTimeout(() => {
153
+        const elm = rFocusable.current
154
+        if (!elm) return
155
+        elm.focus()
156
+      }, 0)
157
+    }
158
+  }, [isEditing])
159
+
160
+  return shapeUtils.render(shape, { isEditing, ref: rFocusable })
120 161
 })
121 162
 
122 163
 const StyledShape = styled('path', {
@@ -125,12 +166,10 @@ const StyledShape = styled('path', {
125 166
   pointerEvents: 'none',
126 167
 })
127 168
 
128
-const HoverIndicator = styled('path', {
129
-  stroke: '$selected',
169
+const EventSoak = styled('use', {
170
+  opacity: 0,
130 171
   strokeLinecap: 'round',
131 172
   strokeLinejoin: 'round',
132
-  fill: 'transparent',
133
-  filter: 'url(#expand)',
134 173
   variants: {
135 174
     variant: {
136 175
       ghost: {
@@ -150,81 +189,6 @@ const HoverIndicator = styled('path', {
150 189
 
151 190
 const StyledGroup = styled('g', {
152 191
   outline: 'none',
153
-  [`& *[data-shy="true"]`]: {
154
-    opacity: '0',
155
-  },
156
-  [`& ${HoverIndicator}`]: {
157
-    opacity: '0',
158
-  },
159
-  variants: {
160
-    device: {
161
-      mobile: {},
162
-      desktop: {},
163
-    },
164
-    isSelected: {
165
-      true: {
166
-        [`& *[data-shy="true"]`]: {
167
-          opacity: '1',
168
-        },
169
-        [`& ${HoverIndicator}`]: {
170
-          opacity: '0.2',
171
-        },
172
-      },
173
-      false: {
174
-        [`& ${HoverIndicator}`]: {
175
-          opacity: '0',
176
-        },
177
-      },
178
-    },
179
-  },
180
-  compoundVariants: [
181
-    {
182
-      device: 'desktop',
183
-      isSelected: 'false',
184
-      css: {
185
-        [`&:hover ${HoverIndicator}`]: {
186
-          opacity: '0.16',
187
-        },
188
-        [`&:hover *[data-shy="true"]`]: {
189
-          opacity: '1',
190
-        },
191
-      },
192
-    },
193
-    {
194
-      device: 'desktop',
195
-      isSelected: 'true',
196
-      css: {
197
-        [`&:hover ${HoverIndicator}`]: {
198
-          opacity: '0.25',
199
-        },
200
-        [`&:active ${HoverIndicator}`]: {
201
-          opacity: '0.25',
202
-        },
203
-      },
204
-    },
205
-  ],
206 192
 })
207 193
 
208
-// function Label({ children }: { children: React.ReactNode }) {
209
-//   return (
210
-//     <text
211
-//       y={4}
212
-//       x={4}
213
-//       fontSize={12}
214
-//       fill="black"
215
-//       stroke="none"
216
-//       alignmentBaseline="text-before-edge"
217
-//       pointerEvents="none"
218
-//     >
219
-//       {children}
220
-//     </text>
221
-//   )
222
-// }
223
-
224
-// function pp(n: number[]) {
225
-//   return '[' + n.map((v) => v.toFixed(1)).join(', ') + ']'
226
-// }
227
-
228
-export { HoverIndicator }
229
-
230 194
 export default memo(Shape)

+ 14
- 14
components/controls-panel/controls-panel.tsx 查看文件

@@ -3,40 +3,44 @@ import styled from 'styles'
3 3
 import React, { useRef } from 'react'
4 4
 import state, { useSelector } from 'state'
5 5
 import { X, Code } from 'react-feather'
6
-import { IconButton } from 'components/shared'
6
+import { breakpoints, IconButton } from 'components/shared'
7 7
 import * as Panel from '../panel'
8 8
 import Control from './control'
9 9
 import { deepCompareArrays } from 'utils'
10 10
 
11
+function openCodePanel() {
12
+  state.send('CLOSED_CODE_PANEL')
13
+}
14
+
15
+function closeCodePanel() {
16
+  state.send('OPENED_CODE_PANEL')
17
+}
18
+
11 19
 const stopKeyboardPropagation = (e: KeyboardEvent | React.KeyboardEvent) =>
12 20
   e.stopPropagation()
13 21
 
14 22
 export default function ControlPanel(): JSX.Element {
15 23
   const rContainer = useRef<HTMLDivElement>(null)
24
+  const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0)
16 25
   const codeControls = useSelector(
17 26
     (state) => Object.keys(state.data.codeControls),
18 27
     deepCompareArrays
19 28
   )
20
-  const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0)
21 29
 
22 30
   return (
23 31
     <Panel.Root
32
+      ref={rContainer}
24 33
       dir="ltr"
25 34
       data-bp-desktop
26
-      ref={rContainer}
27
-      isOpen={isOpen}
28 35
       variant="controls"
36
+      isOpen={isOpen}
29 37
       onKeyDown={stopKeyboardPropagation}
30 38
       onKeyUp={stopKeyboardPropagation}
31 39
     >
32 40
       {isOpen ? (
33 41
         <Panel.Layout>
34 42
           <Panel.Header>
35
-            <IconButton
36
-              bp={{ '@initial': 'mobile', '@sm': 'small' }}
37
-              size="small"
38
-              onClick={() => state.send('CLOSED_CODE_PANEL')}
39
-            >
43
+            <IconButton bp={breakpoints} size="small" onClick={closeCodePanel}>
40 44
               <X />
41 45
             </IconButton>
42 46
             <h3>Controls</h3>
@@ -48,11 +52,7 @@ export default function ControlPanel(): JSX.Element {
48 52
           </ControlsList>
49 53
         </Panel.Layout>
50 54
       ) : (
51
-        <IconButton
52
-          bp={{ '@initial': 'mobile', '@sm': 'small' }}
53
-          size="small"
54
-          onClick={() => state.send('OPENED_CODE_PANEL')}
55
-        >
55
+        <IconButton bp={breakpoints} size="small" onClick={openCodePanel}>
56 56
           <Code />
57 57
         </IconButton>
58 58
       )}

+ 5
- 9
components/page-panel/page-panel.tsx 查看文件

@@ -2,7 +2,7 @@ import styled from 'styles'
2 2
 import * as ContextMenu from '@radix-ui/react-context-menu'
3 3
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
4 4
 
5
-import { IconWrapper, RowButton } from 'components/shared'
5
+import { breakpoints, IconWrapper, RowButton } from 'components/shared'
6 6
 import { CheckIcon, PlusIcon } from '@radix-ui/react-icons'
7 7
 import * as Panel from '../panel'
8 8
 import state, { useSelector } from 'state'
@@ -40,8 +40,8 @@ export default function PagePanel(): JSX.Element {
40 40
       <PanelRoot dir="ltr">
41 41
         <DropdownMenu.Trigger
42 42
           as={RowButton}
43
-          bp={{ '@initial': 'mobile', '@sm': 'small' }}
44
-          css={{ paddingRight: 12 }}
43
+          bp={breakpoints}
44
+          variant="pageButton"
45 45
         >
46 46
           <span>{documentPages[currentPageId].name}</span>
47 47
         </DropdownMenu.Trigger>
@@ -58,11 +58,7 @@ export default function PagePanel(): JSX.Element {
58 58
               {sorted.map(({ id, name }) => (
59 59
                 <ContextMenu.Root dir="ltr" key={id}>
60 60
                   <ContextMenu.Trigger>
61
-                    <StyledRadioItem
62
-                      key={id}
63
-                      value={id}
64
-                      bp={{ '@initial': 'mobile', '@sm': 'small' }}
65
-                    >
61
+                    <StyledRadioItem key={id} value={id} bp={breakpoints}>
66 62
                       <span>{name}</span>
67 63
                       <DropdownMenu.ItemIndicator as={IconWrapper} size="small">
68 64
                         <CheckIcon />
@@ -91,7 +87,7 @@ export default function PagePanel(): JSX.Element {
91 87
             </DropdownMenu.RadioGroup>
92 88
             <DropdownMenu.Separator />
93 89
             <RowButton
94
-              bp={{ '@initial': 'mobile', '@sm': 'small' }}
90
+              bp={breakpoints}
95 91
               onClick={() => {
96 92
                 setIsOpen(false)
97 93
                 state.send('CREATED_PAGE')

+ 7
- 0
components/shared.tsx 查看文件

@@ -3,6 +3,8 @@ import * as RadioGroup from '@radix-ui/react-radio-group'
3 3
 import * as Panel from './panel'
4 4
 import styled from 'styles'
5 5
 
6
+export const breakpoints: any = { '@initial': 'mobile', '@sm': 'small' }
7
+
6 8
 export const IconButton = styled('button', {
7 9
   height: '32px',
8 10
   width: '32px',
@@ -124,6 +126,11 @@ export const RowButton = styled('button', {
124 126
         width: 'auto',
125 127
       },
126 128
     },
129
+    variant: {
130
+      pageButton: {
131
+        paddingRight: 12,
132
+      },
133
+    },
127 134
   },
128 135
 })
129 136
 

+ 15
- 12
components/style-panel/align-distribute.tsx 查看文件

@@ -10,7 +10,8 @@ import {
10 10
   StretchHorizontallyIcon,
11 11
   StretchVerticallyIcon,
12 12
 } from '@radix-ui/react-icons'
13
-import { IconButton } from 'components/shared'
13
+import { breakpoints, IconButton } from 'components/shared'
14
+import { memo } from 'react'
14 15
 import state from 'state'
15 16
 import styled from 'styles'
16 17
 import { AlignType, DistributeType, StretchType } from 'types'
@@ -55,7 +56,7 @@ function distributeHorizontally() {
55 56
   state.send('DISTRIBUTED', { type: DistributeType.Horizontal })
56 57
 }
57 58
 
58
-export default function AlignDistribute({
59
+function AlignDistribute({
59 60
   hasTwoOrMore,
60 61
   hasThreeOrMore,
61 62
 }: {
@@ -65,7 +66,7 @@ export default function AlignDistribute({
65 66
   return (
66 67
     <Container>
67 68
       <IconButton
68
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
69
+        bp={breakpoints}
69 70
         size="small"
70 71
         disabled={!hasTwoOrMore}
71 72
         onClick={alignLeft}
@@ -73,7 +74,7 @@ export default function AlignDistribute({
73 74
         <AlignLeftIcon />
74 75
       </IconButton>
75 76
       <IconButton
76
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
77
+        bp={breakpoints}
77 78
         size="small"
78 79
         disabled={!hasTwoOrMore}
79 80
         onClick={alignCenterHorizontal}
@@ -81,7 +82,7 @@ export default function AlignDistribute({
81 82
         <AlignCenterHorizontallyIcon />
82 83
       </IconButton>
83 84
       <IconButton
84
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
85
+        bp={breakpoints}
85 86
         size="small"
86 87
         disabled={!hasTwoOrMore}
87 88
         onClick={alignRight}
@@ -89,7 +90,7 @@ export default function AlignDistribute({
89 90
         <AlignRightIcon />
90 91
       </IconButton>
91 92
       <IconButton
92
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
93
+        bp={breakpoints}
93 94
         size="small"
94 95
         disabled={!hasTwoOrMore}
95 96
         onClick={stretchHorizontally}
@@ -97,7 +98,7 @@ export default function AlignDistribute({
97 98
         <StretchHorizontallyIcon />
98 99
       </IconButton>
99 100
       <IconButton
100
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
101
+        bp={breakpoints}
101 102
         size="small"
102 103
         disabled={!hasThreeOrMore}
103 104
         onClick={distributeHorizontally}
@@ -105,7 +106,7 @@ export default function AlignDistribute({
105 106
         <SpaceEvenlyHorizontallyIcon />
106 107
       </IconButton>
107 108
       <IconButton
108
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
109
+        bp={breakpoints}
109 110
         size="small"
110 111
         disabled={!hasTwoOrMore}
111 112
         onClick={alignTop}
@@ -113,7 +114,7 @@ export default function AlignDistribute({
113 114
         <AlignTopIcon />
114 115
       </IconButton>
115 116
       <IconButton
116
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
117
+        bp={breakpoints}
117 118
         size="small"
118 119
         disabled={!hasTwoOrMore}
119 120
         onClick={alignCenterVertical}
@@ -121,7 +122,7 @@ export default function AlignDistribute({
121 122
         <AlignCenterVerticallyIcon />
122 123
       </IconButton>
123 124
       <IconButton
124
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
125
+        bp={breakpoints}
125 126
         size="small"
126 127
         disabled={!hasTwoOrMore}
127 128
         onClick={alignBottom}
@@ -129,7 +130,7 @@ export default function AlignDistribute({
129 130
         <AlignBottomIcon />
130 131
       </IconButton>
131 132
       <IconButton
132
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
133
+        bp={breakpoints}
133 134
         size="small"
134 135
         disabled={!hasTwoOrMore}
135 136
         onClick={stretchVertically}
@@ -137,7 +138,7 @@ export default function AlignDistribute({
137 138
         <StretchVerticallyIcon />
138 139
       </IconButton>
139 140
       <IconButton
140
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
141
+        bp={breakpoints}
141 142
         size="small"
142 143
         disabled={!hasThreeOrMore}
143 144
         onClick={distributeVertically}
@@ -148,6 +149,8 @@ export default function AlignDistribute({
148 149
   )
149 150
 }
150 151
 
152
+export default memo(AlignDistribute)
153
+
151 154
 const Container = styled('div', {
152 155
   display: 'grid',
153 156
   padding: 4,

+ 13
- 6
components/style-panel/color-content.tsx 查看文件

@@ -4,12 +4,16 @@ import { ColorStyle } from 'types'
4 4
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
5 5
 import { Square } from 'react-feather'
6 6
 import { DropdownContent } from '../shared'
7
+import { memo } from 'react'
8
+import state from 'state'
7 9
 
8
-export default function ColorContent({
9
-  onChange,
10
-}: {
11
-  onChange: (color: ColorStyle) => void
12
-}): JSX.Element {
10
+function handleColorChange(
11
+  e: Event & { currentTarget: { value: ColorStyle } }
12
+): void {
13
+  state.send('CHANGED_STYLE', { color: e.currentTarget.value })
14
+}
15
+
16
+function ColorContent(): JSX.Element {
13 17
   return (
14 18
     <DropdownContent sideOffset={8} side="bottom">
15 19
       {Object.keys(strokes).map((color: ColorStyle) => (
@@ -17,7 +21,8 @@ export default function ColorContent({
17 21
           as={IconButton}
18 22
           key={color}
19 23
           title={color}
20
-          onSelect={() => onChange(color)}
24
+          value={color}
25
+          onSelect={handleColorChange}
21 26
         >
22 27
           <Square fill={strokes[color]} stroke="none" size="22" />
23 28
         </DropdownMenu.DropdownMenuItem>
@@ -25,3 +30,5 @@ export default function ColorContent({
25 30
     </DropdownContent>
26 31
   )
27 32
 }
33
+
34
+export default memo(ColorContent)

+ 9
- 12
components/style-panel/color-picker.tsx 查看文件

@@ -1,28 +1,25 @@
1 1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2 2
 import { strokes } from 'state/shape-styles'
3
-import { ColorStyle } from 'types'
4
-import { RowButton, IconWrapper } from '../shared'
3
+import { RowButton, IconWrapper, breakpoints } from '../shared'
5 4
 import { Square } from 'react-feather'
6 5
 import ColorContent from './color-content'
6
+import { memo } from 'react'
7
+import { useSelector } from 'state'
7 8
 
8
-interface Props {
9
-  color: ColorStyle
10
-  onChange: (color: ColorStyle) => void
11
-}
9
+function ColorPicker(): JSX.Element {
10
+  const color = useSelector((s) => s.values.selectedStyle.color)
12 11
 
13
-export default function ColorPicker({ color, onChange }: Props): JSX.Element {
14 12
   return (
15 13
     <DropdownMenu.Root dir="ltr">
16
-      <DropdownMenu.Trigger
17
-        as={RowButton}
18
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
19
-      >
14
+      <DropdownMenu.Trigger as={RowButton} bp={breakpoints}>
20 15
         <label htmlFor="color">Color</label>
21 16
         <IconWrapper>
22 17
           <Square fill={strokes[color]} />
23 18
         </IconWrapper>
24 19
       </DropdownMenu.Trigger>
25
-      <ColorContent onChange={onChange} />
20
+      <ColorContent />
26 21
     </DropdownMenu.Root>
27 22
   )
28 23
 }
24
+
25
+export default memo(ColorPicker)

+ 21
- 25
components/style-panel/dash-picker.tsx 查看文件

@@ -7,40 +7,36 @@ import {
7 7
 } from '../shared'
8 8
 import * as RadioGroup from '@radix-ui/react-radio-group'
9 9
 import { DashStyle } from 'types'
10
-import state from 'state'
10
+import state, { useSelector } from 'state'
11
+import { memo } from 'react'
11 12
 
12 13
 function handleChange(dash: string) {
13 14
   state.send('CHANGED_STYLE', { dash })
14 15
 }
15 16
 
16
-interface Props {
17
-  dash: DashStyle
17
+const dashes = {
18
+  [DashStyle.Solid]: <DashSolidIcon />,
19
+  [DashStyle.Dashed]: <DashDashedIcon />,
20
+  [DashStyle.Dotted]: <DashDottedIcon />,
18 21
 }
19 22
 
20
-export default function DashPicker({ dash }: Props): JSX.Element {
23
+function DashPicker(): JSX.Element {
24
+  const dash = useSelector((s) => s.values.selectedStyle.dash)
25
+
21 26
   return (
22 27
     <Group name="Dash" onValueChange={handleChange}>
23
-      <Item
24
-        as={RadioGroup.RadioGroupItem}
25
-        value={DashStyle.Solid}
26
-        isActive={dash === DashStyle.Solid}
27
-      >
28
-        <DashSolidIcon />
29
-      </Item>
30
-      <Item
31
-        as={RadioGroup.RadioGroupItem}
32
-        value={DashStyle.Dashed}
33
-        isActive={dash === DashStyle.Dashed}
34
-      >
35
-        <DashDashedIcon />
36
-      </Item>
37
-      <Item
38
-        as={RadioGroup.RadioGroupItem}
39
-        value={DashStyle.Dotted}
40
-        isActive={dash === DashStyle.Dotted}
41
-      >
42
-        <DashDottedIcon />
43
-      </Item>
28
+      {Object.keys(DashStyle).map((dashStyle: DashStyle) => (
29
+        <RadioGroup.RadioGroupItem
30
+          as={Item}
31
+          key={dashStyle}
32
+          isActive={dash === dashStyle}
33
+          value={dashStyle}
34
+        >
35
+          {dashes[dashStyle]}
36
+        </RadioGroup.RadioGroupItem>
37
+      ))}
44 38
     </Group>
45 39
   )
46 40
 }
41
+
42
+export default memo(DashPicker)

+ 9
- 10
components/style-panel/is-filled-picker.tsx 查看文件

@@ -2,24 +2,23 @@ import * as Checkbox from '@radix-ui/react-checkbox'
2 2
 import { CheckIcon } from '@radix-ui/react-icons'
3 3
 import { strokes } from 'state/shape-styles'
4 4
 import { Square } from 'react-feather'
5
-import { IconWrapper, RowButton } from '../shared'
5
+import { breakpoints, IconWrapper, RowButton } from '../shared'
6
+import state, { useSelector } from 'state'
6 7
 
7
-interface Props {
8
-  isFilled: boolean
9
-  onChange: (isFilled: boolean | string) => void
8
+function handleIsFilledChange(isFilled: boolean) {
9
+  state.send('CHANGED_STYLE', { isFilled })
10 10
 }
11 11
 
12
-export default function IsFilledPicker({
13
-  isFilled,
14
-  onChange,
15
-}: Props): JSX.Element {
12
+export default function IsFilledPicker(): JSX.Element {
13
+  const isFilled = useSelector((s) => s.values.selectedStyle.isFilled)
14
+
16 15
   return (
17 16
     <Checkbox.Root
18 17
       dir="ltr"
19 18
       as={RowButton}
20
-      bp={{ '@initial': 'mobile', '@sm': 'small' }}
19
+      bp={breakpoints}
21 20
       checked={isFilled}
22
-      onCheckedChange={onChange}
21
+      onCheckedChange={handleIsFilledChange}
23 22
     >
24 23
       <label htmlFor="fill">Fill</label>
25 24
       <IconWrapper>

+ 4
- 9
components/style-panel/quick-color-select.tsx 查看文件

@@ -1,9 +1,9 @@
1 1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
-import { IconButton } from 'components/shared'
2
+import { breakpoints, IconButton } from 'components/shared'
3 3
 import Tooltip from 'components/tooltip'
4 4
 import { strokes } from 'state/shape-styles'
5 5
 import { Square } from 'react-feather'
6
-import state, { useSelector } from 'state'
6
+import { useSelector } from 'state'
7 7
 import ColorContent from './color-content'
8 8
 
9 9
 export default function QuickColorSelect(): JSX.Element {
@@ -11,17 +11,12 @@ export default function QuickColorSelect(): JSX.Element {
11 11
 
12 12
   return (
13 13
     <DropdownMenu.Root dir="ltr">
14
-      <DropdownMenu.Trigger
15
-        as={IconButton}
16
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
17
-      >
14
+      <DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
18 15
         <Tooltip label="Color">
19 16
           <Square fill={strokes[color]} stroke={strokes[color]} />
20 17
         </Tooltip>
21 18
       </DropdownMenu.Trigger>
22
-      <ColorContent
23
-        onChange={(color) => state.send('CHANGED_STYLE', { color })}
24
-      />
19
+      <ColorContent />
25 20
     </DropdownMenu.Root>
26 21
   )
27 22
 }

+ 22
- 26
components/style-panel/quick-dash-select.tsx 查看文件

@@ -1,6 +1,7 @@
1 1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
-import { IconButton } from 'components/shared'
2
+import { breakpoints, IconButton } from 'components/shared'
3 3
 import Tooltip from 'components/tooltip'
4
+import { memo } from 'react'
4 5
 import state, { useSelector } from 'state'
5 6
 import { DashStyle } from 'types'
6 7
 import {
@@ -17,40 +18,35 @@ const dashes = {
17 18
   [DashStyle.Dotted]: <DashDottedIcon />,
18 19
 }
19 20
 
20
-export default function QuickdashSelect(): JSX.Element {
21
+function changeDashStyle(
22
+  e: Event & { currentTarget: { value: DashStyle } }
23
+): void {
24
+  state.send('CHANGED_STYLE', { dash: e.currentTarget.value })
25
+}
26
+
27
+function QuickdashSelect(): JSX.Element {
21 28
   const dash = useSelector((s) => s.values.selectedStyle.dash)
22 29
 
23 30
   return (
24 31
     <DropdownMenu.Root dir="ltr">
25
-      <DropdownMenu.Trigger
26
-        as={IconButton}
27
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
28
-      >
32
+      <DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
29 33
         <Tooltip label="Dash">{dashes[dash]}</Tooltip>
30 34
       </DropdownMenu.Trigger>
31 35
       <DropdownContent sideOffset={8} direction="vertical">
32
-        <DashItem isActive={dash === DashStyle.Solid} dash={DashStyle.Solid} />
33
-        <DashItem
34
-          isActive={dash === DashStyle.Dashed}
35
-          dash={DashStyle.Dashed}
36
-        />
37
-        <DashItem
38
-          isActive={dash === DashStyle.Dotted}
39
-          dash={DashStyle.Dotted}
40
-        />
36
+        {Object.keys(DashStyle).map((dashStyle: DashStyle) => (
37
+          <DropdownMenu.DropdownMenuItem
38
+            as={Item}
39
+            key={dashStyle}
40
+            isActive={dash === dashStyle}
41
+            onSelect={changeDashStyle}
42
+            value={dashStyle}
43
+          >
44
+            {dashes[dashStyle]}
45
+          </DropdownMenu.DropdownMenuItem>
46
+        ))}
41 47
       </DropdownContent>
42 48
     </DropdownMenu.Root>
43 49
   )
44 50
 }
45 51
 
46
-function DashItem({ dash, isActive }: { isActive: boolean; dash: DashStyle }) {
47
-  return (
48
-    <Item
49
-      as={DropdownMenu.DropdownMenuItem}
50
-      isActive={isActive}
51
-      onSelect={() => state.send('CHANGED_STYLE', { dash })}
52
-    >
53
-      {dashes[dash]}
54
-    </Item>
55
-  )
56
-}
52
+export default memo(QuickdashSelect)

+ 22
- 23
components/style-panel/quick-size-select.tsx 查看文件

@@ -1,6 +1,7 @@
1 1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
-import { IconButton } from 'components/shared'
2
+import { breakpoints, IconButton } from 'components/shared'
3 3
 import Tooltip from 'components/tooltip'
4
+import { memo } from 'react'
4 5
 import { Circle } from 'react-feather'
5 6
 import state, { useSelector } from 'state'
6 7
 import { SizeStyle } from 'types'
@@ -12,39 +13,37 @@ const sizes = {
12 13
   [SizeStyle.Large]: 22,
13 14
 }
14 15
 
15
-export default function QuickSizeSelect(): JSX.Element {
16
+function handleSizeChange(
17
+  e: Event & { currentTarget: { value: SizeStyle } }
18
+): void {
19
+  state.send('CHANGED_STYLE', { size: e.currentTarget.value })
20
+}
21
+
22
+function QuickSizeSelect(): JSX.Element {
16 23
   const size = useSelector((s) => s.values.selectedStyle.size)
17 24
 
18 25
   return (
19 26
     <DropdownMenu.Root dir="ltr">
20
-      <DropdownMenu.Trigger
21
-        as={IconButton}
22
-        bp={{ '@initial': 'mobile', '@sm': 'small' }}
23
-      >
27
+      <DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
24 28
         <Tooltip label="Size">
25 29
           <Circle size={sizes[size]} stroke="none" fill="currentColor" />
26 30
         </Tooltip>
27 31
       </DropdownMenu.Trigger>
28 32
       <DropdownContent sideOffset={8} direction="vertical">
29
-        <SizeItem isActive={size === SizeStyle.Small} size={SizeStyle.Small} />
30
-        <SizeItem
31
-          isActive={size === SizeStyle.Medium}
32
-          size={SizeStyle.Medium}
33
-        />
34
-        <SizeItem isActive={size === SizeStyle.Large} size={SizeStyle.Large} />
33
+        {Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => (
34
+          <DropdownMenu.DropdownMenuItem
35
+            key={sizeStyle}
36
+            as={Item}
37
+            isActive={size === sizeStyle}
38
+            value={sizeStyle}
39
+            onSelect={handleSizeChange}
40
+          >
41
+            <Circle size={sizes[sizeStyle]} />
42
+          </DropdownMenu.DropdownMenuItem>
43
+        ))}
35 44
       </DropdownContent>
36 45
     </DropdownMenu.Root>
37 46
   )
38 47
 }
39 48
 
40
-function SizeItem({ size, isActive }: { isActive: boolean; size: SizeStyle }) {
41
-  return (
42
-    <Item
43
-      as={DropdownMenu.DropdownMenuItem}
44
-      isActive={isActive}
45
-      onSelect={() => state.send('CHANGED_STYLE', { size })}
46
-    >
47
-      <Circle size={sizes[size]} />
48
-    </Item>
49
-  )
50
-}
49
+export default memo(QuickSizeSelect)

+ 217
- 0
components/style-panel/shapes-functions.tsx 查看文件

@@ -0,0 +1,217 @@
1
+import { IconButton, breakpoints } from 'components/shared'
2
+import { memo } from 'react'
3
+import styled from 'styles'
4
+import { MoveType } from 'types'
5
+import { Trash2 } from 'react-feather'
6
+import state, { useSelector } from 'state'
7
+import Tooltip from 'components/tooltip'
8
+
9
+import {
10
+  ArrowDownIcon,
11
+  ArrowUpIcon,
12
+  AspectRatioIcon,
13
+  BoxIcon,
14
+  CopyIcon,
15
+  EyeClosedIcon,
16
+  EyeOpenIcon,
17
+  LockClosedIcon,
18
+  LockOpen1Icon,
19
+  PinBottomIcon,
20
+  PinTopIcon,
21
+  RotateCounterClockwiseIcon,
22
+} from '@radix-ui/react-icons'
23
+import { getPage, getSelectedIds } from 'utils'
24
+
25
+function handleRotateCcw() {
26
+  state.send('ROTATED_CCW')
27
+}
28
+
29
+function handleDuplicate() {
30
+  state.send('DUPLICATED')
31
+}
32
+
33
+function handleHide() {
34
+  state.send('TOGGLED_SHAPE_HIDE')
35
+}
36
+
37
+function handleLock() {
38
+  state.send('TOGGLED_SHAPE_LOCK')
39
+}
40
+
41
+function handleAspectLock() {
42
+  state.send('TOGGLED_SHAPE_ASPECT_LOCK')
43
+}
44
+
45
+function handleMoveToBack() {
46
+  state.send('MOVED', { type: MoveType.ToBack })
47
+}
48
+
49
+function handleMoveBackward() {
50
+  state.send('MOVED', { type: MoveType.Backward })
51
+}
52
+
53
+function handleMoveForward() {
54
+  state.send('MOVED', { type: MoveType.Forward })
55
+}
56
+
57
+function handleMoveToFront() {
58
+  state.send('MOVED', { type: MoveType.ToFront })
59
+}
60
+
61
+function handleDelete() {
62
+  state.send('DELETED')
63
+}
64
+
65
+function ShapesFunctions() {
66
+  const isAllLocked = useSelector((s) => {
67
+    const page = getPage(s.data)
68
+    return s.values.selectedIds.every((id) => page.shapes[id].isLocked)
69
+  })
70
+
71
+  const isAllAspectLocked = useSelector((s) => {
72
+    const page = getPage(s.data)
73
+    return s.values.selectedIds.every(
74
+      (id) => page.shapes[id].isAspectRatioLocked
75
+    )
76
+  })
77
+
78
+  const isAllHidden = useSelector((s) => {
79
+    const page = getPage(s.data)
80
+    return s.values.selectedIds.every((id) => page.shapes[id].isHidden)
81
+  })
82
+
83
+  const hasSelection = useSelector((s) => {
84
+    return getSelectedIds(s.data).size > 0
85
+  })
86
+
87
+  return (
88
+    <>
89
+      <ButtonsRow>
90
+        <IconButton
91
+          bp={breakpoints}
92
+          disabled={!hasSelection}
93
+          size="small"
94
+          onClick={handleDuplicate}
95
+        >
96
+          <Tooltip label="Duplicate">
97
+            <CopyIcon />
98
+          </Tooltip>
99
+        </IconButton>
100
+
101
+        <IconButton
102
+          disabled={!hasSelection}
103
+          size="small"
104
+          onClick={handleRotateCcw}
105
+        >
106
+          <Tooltip label="Rotate">
107
+            <RotateCounterClockwiseIcon />
108
+          </Tooltip>
109
+        </IconButton>
110
+
111
+        <IconButton
112
+          bp={breakpoints}
113
+          disabled={!hasSelection}
114
+          size="small"
115
+          onClick={handleHide}
116
+        >
117
+          <Tooltip label="Toogle Hidden">
118
+            {isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
119
+          </Tooltip>
120
+        </IconButton>
121
+
122
+        <IconButton
123
+          bp={breakpoints}
124
+          disabled={!hasSelection}
125
+          size="small"
126
+          onClick={handleLock}
127
+        >
128
+          <Tooltip label="Toogle Locked">
129
+            {isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
130
+          </Tooltip>
131
+        </IconButton>
132
+
133
+        <IconButton
134
+          bp={breakpoints}
135
+          disabled={!hasSelection}
136
+          size="small"
137
+          onClick={handleAspectLock}
138
+        >
139
+          <Tooltip label="Toogle Aspect Ratio Lock">
140
+            {isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
141
+          </Tooltip>
142
+        </IconButton>
143
+      </ButtonsRow>
144
+      <ButtonsRow>
145
+        <IconButton
146
+          bp={breakpoints}
147
+          disabled={!hasSelection}
148
+          size="small"
149
+          onClick={handleMoveToBack}
150
+        >
151
+          <Tooltip label="Move to Back">
152
+            <PinBottomIcon />
153
+          </Tooltip>
154
+        </IconButton>
155
+
156
+        <IconButton
157
+          bp={breakpoints}
158
+          disabled={!hasSelection}
159
+          size="small"
160
+          onClick={handleMoveBackward}
161
+        >
162
+          <Tooltip label="Move Backward">
163
+            <ArrowDownIcon />
164
+          </Tooltip>
165
+        </IconButton>
166
+
167
+        <IconButton
168
+          bp={breakpoints}
169
+          disabled={!hasSelection}
170
+          size="small"
171
+          onClick={handleMoveForward}
172
+        >
173
+          <Tooltip label="Move Forward">
174
+            <ArrowUpIcon />
175
+          </Tooltip>
176
+        </IconButton>
177
+
178
+        <IconButton
179
+          bp={breakpoints}
180
+          disabled={!hasSelection}
181
+          size="small"
182
+          onClick={handleMoveToFront}
183
+        >
184
+          <Tooltip label="More to Front">
185
+            <PinTopIcon />
186
+          </Tooltip>
187
+        </IconButton>
188
+
189
+        <IconButton
190
+          bp={breakpoints}
191
+          disabled={!hasSelection}
192
+          size="small"
193
+          onClick={handleDelete}
194
+        >
195
+          <Tooltip label="Delete">
196
+            <Trash2 size="15" />
197
+          </Tooltip>
198
+        </IconButton>
199
+      </ButtonsRow>
200
+    </>
201
+  )
202
+}
203
+
204
+export default memo(ShapesFunctions)
205
+
206
+const ButtonsRow = styled('div', {
207
+  position: 'relative',
208
+  display: 'flex',
209
+  width: '100%',
210
+  background: 'none',
211
+  border: 'none',
212
+  cursor: 'pointer',
213
+  outline: 'none',
214
+  alignItems: 'center',
215
+  justifyContent: 'flex-start',
216
+  padding: 4,
217
+})

+ 23
- 23
components/style-panel/size-picker.tsx 查看文件

@@ -1,37 +1,37 @@
1 1
 import { Group, Item } from '../shared'
2 2
 import * as RadioGroup from '@radix-ui/react-radio-group'
3 3
 import { Circle } from 'react-feather'
4
-import state from 'state'
4
+import state, { useSelector } from 'state'
5 5
 import { SizeStyle } from 'types'
6
+import { memo } from 'react'
7
+
8
+const sizes = {
9
+  [SizeStyle.Small]: 6,
10
+  [SizeStyle.Medium]: 12,
11
+  [SizeStyle.Large]: 22,
12
+}
6 13
 
7 14
 function handleChange(size: string) {
8 15
   state.send('CHANGED_STYLE', { size })
9 16
 }
10 17
 
11
-export default function SizePicker({ size }: { size: SizeStyle }): JSX.Element {
18
+function SizePicker(): JSX.Element {
19
+  const size = useSelector((s) => s.values.selectedStyle.size)
20
+
12 21
   return (
13 22
     <Group name="width" onValueChange={handleChange}>
14
-      <Item
15
-        as={RadioGroup.Item}
16
-        value={SizeStyle.Small}
17
-        isActive={size === SizeStyle.Small}
18
-      >
19
-        <Circle size={6} />
20
-      </Item>
21
-      <Item
22
-        as={RadioGroup.Item}
23
-        value={SizeStyle.Medium}
24
-        isActive={size === SizeStyle.Medium}
25
-      >
26
-        <Circle size={12} />
27
-      </Item>
28
-      <Item
29
-        as={RadioGroup.Item}
30
-        value={SizeStyle.Large}
31
-        isActive={size === SizeStyle.Large}
32
-      >
33
-        <Circle size={22} />
34
-      </Item>
23
+      {Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => (
24
+        <RadioGroup.Item
25
+          key={sizeStyle}
26
+          as={Item}
27
+          isActive={size === sizeStyle}
28
+          value={sizeStyle}
29
+        >
30
+          <Circle size={sizes[sizeStyle]} />
31
+        </RadioGroup.Item>
32
+      ))}
35 33
     </Group>
36 34
   )
37 35
 }
36
+
37
+export default memo(SizePicker)

+ 12
- 192
components/style-panel/style-panel.tsx 查看文件

@@ -3,31 +3,10 @@ import state, { useSelector } from 'state'
3 3
 import * as Panel from 'components/panel'
4 4
 import { useRef } from 'react'
5 5
 import { IconButton } from 'components/shared'
6
-import { ChevronDown, Trash2, X } from 'react-feather'
7
-import {
8
-  deepCompare,
9
-  deepCompareArrays,
10
-  getPage,
11
-  getSelectedIds,
12
-  setToArray,
13
-} from 'utils'
6
+import { ChevronDown, X } from 'react-feather'
7
+import ShapesFunctions from './shapes-functions'
14 8
 import AlignDistribute from './align-distribute'
15
-import { MoveType } from 'types'
16 9
 import SizePicker from './size-picker'
17
-import {
18
-  ArrowDownIcon,
19
-  ArrowUpIcon,
20
-  AspectRatioIcon,
21
-  BoxIcon,
22
-  CopyIcon,
23
-  EyeClosedIcon,
24
-  EyeOpenIcon,
25
-  LockClosedIcon,
26
-  LockOpen1Icon,
27
-  PinBottomIcon,
28
-  PinTopIcon,
29
-  RotateCounterClockwiseIcon,
30
-} from '@radix-ui/react-icons'
31 10
 import DashPicker from './dash-picker'
32 11
 import QuickColorSelect from './quick-color-select'
33 12
 import ColorPicker from './color-picker'
@@ -37,23 +16,12 @@ import QuickdashSelect from './quick-dash-select'
37 16
 import Tooltip from 'components/tooltip'
38 17
 
39 18
 const breakpoints = { '@initial': 'mobile', '@sm': 'small' } as any
19
+
40 20
 const handleStylePanelOpen = () => state.send('TOGGLED_STYLE_PANEL_OPEN')
41
-const handleColorChange = (color) => state.send('CHANGED_STYLE', { color })
42
-const handleRotateCcw = () => () => state.send('ROTATED_CCW')
43
-const handleIsFilledChange = (dash) => state.send('CHANGED_STYLE', { dash })
44
-const handleDuplicate = () => state.send('DUPLICATED')
45
-const handleHide = () => state.send('TOGGLED_SHAPE_HIDE')
46
-const handleLock = () => state.send('TOGGLED_SHAPE_LOCK')
47
-const handleAspectLock = () => state.send('TOGGLED_SHAPE_ASPECT_LOCK')
48
-const handleMoveToBack = () => state.send('MOVED', { type: MoveType.ToBack })
49
-const handleMoveBackward = () =>
50
-  state.send('MOVED', { type: MoveType.Backward })
51
-const handleMoveForward = () => state.send('MOVED', { type: MoveType.Forward })
52
-const handleMoveToFront = () => state.send('MOVED', { type: MoveType.ToFront })
53
-const handleDelete = () => state.send('DELETED')
54 21
 
55 22
 export default function StylePanel(): JSX.Element {
56 23
   const rContainer = useRef<HTMLDivElement>(null)
24
+
57 25
   const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
58 26
 
59 27
   return (
@@ -86,29 +54,7 @@ export default function StylePanel(): JSX.Element {
86 54
 // track of this data manually within our state.
87 55
 
88 56
 function SelectedShapeStyles(): JSX.Element {
89
-  const selectedIds = useSelector(
90
-    (s) => setToArray(getSelectedIds(s.data)),
91
-    deepCompareArrays
92
-  )
93
-
94
-  const isAllLocked = useSelector((s) => {
95
-    const page = getPage(s.data)
96
-    return selectedIds.every((id) => page.shapes[id].isLocked)
97
-  })
98
-
99
-  const isAllAspectLocked = useSelector((s) => {
100
-    const page = getPage(s.data)
101
-    return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
102
-  })
103
-
104
-  const isAllHidden = useSelector((s) => {
105
-    const page = getPage(s.data)
106
-    return selectedIds.every((id) => page.shapes[id].isHidden)
107
-  })
108
-
109
-  const commonStyle = useSelector((s) => s.values.selectedStyle, deepCompare)
110
-
111
-  const hasSelection = selectedIds.length > 0
57
+  const selectedShapesCount = useSelector((s) => s.values.selectedIds.length)
112 58
 
113 59
   return (
114 60
     <Panel.Layout>
@@ -123,133 +69,20 @@ function SelectedShapeStyles(): JSX.Element {
123 69
         </IconButton>
124 70
       </Panel.Header>
125 71
       <Content>
126
-        <ColorPicker color={commonStyle.color} onChange={handleColorChange} />
127
-        <IsFilledPicker
128
-          isFilled={commonStyle.isFilled}
129
-          onChange={handleIsFilledChange}
130
-        />
72
+        <ColorPicker />
73
+        <IsFilledPicker />
131 74
         <Row>
132 75
           <label htmlFor="size">Size</label>
133
-          <SizePicker size={commonStyle.size} />
76
+          <SizePicker />
134 77
         </Row>
135 78
         <Row>
136 79
           <label htmlFor="dash">Dash</label>
137
-          <DashPicker dash={commonStyle.dash} />
80
+          <DashPicker />
138 81
         </Row>
139
-        <ButtonsRow>
140
-          <IconButton
141
-            bp={breakpoints}
142
-            disabled={!hasSelection}
143
-            size="small"
144
-            onClick={handleDuplicate}
145
-          >
146
-            <Tooltip label="Duplicate">
147
-              <CopyIcon />
148
-            </Tooltip>
149
-          </IconButton>
150
-
151
-          <IconButton
152
-            disabled={!hasSelection}
153
-            size="small"
154
-            onClick={handleRotateCcw}
155
-          >
156
-            <Tooltip label="Rotate">
157
-              <RotateCounterClockwiseIcon />
158
-            </Tooltip>
159
-          </IconButton>
160
-
161
-          <IconButton
162
-            bp={breakpoints}
163
-            disabled={!hasSelection}
164
-            size="small"
165
-            onClick={handleHide}
166
-          >
167
-            <Tooltip label="Toogle Hidden">
168
-              {isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
169
-            </Tooltip>
170
-          </IconButton>
171
-
172
-          <IconButton
173
-            bp={breakpoints}
174
-            disabled={!hasSelection}
175
-            size="small"
176
-            onClick={handleLock}
177
-          >
178
-            <Tooltip label="Toogle Locked">
179
-              {isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
180
-            </Tooltip>
181
-          </IconButton>
182
-
183
-          <IconButton
184
-            bp={breakpoints}
185
-            disabled={!hasSelection}
186
-            size="small"
187
-            onClick={handleAspectLock}
188
-          >
189
-            <Tooltip label="Toogle Aspect Ratio Lock">
190
-              {isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
191
-            </Tooltip>
192
-          </IconButton>
193
-        </ButtonsRow>
194
-        <ButtonsRow>
195
-          <IconButton
196
-            bp={breakpoints}
197
-            disabled={!hasSelection}
198
-            size="small"
199
-            onClick={handleMoveToBack}
200
-          >
201
-            <Tooltip label="Move to Back">
202
-              <PinBottomIcon />
203
-            </Tooltip>
204
-          </IconButton>
205
-
206
-          <IconButton
207
-            bp={breakpoints}
208
-            disabled={!hasSelection}
209
-            size="small"
210
-            onClick={handleMoveBackward}
211
-          >
212
-            <Tooltip label="Move Backward">
213
-              <ArrowDownIcon />
214
-            </Tooltip>
215
-          </IconButton>
216
-
217
-          <IconButton
218
-            bp={breakpoints}
219
-            disabled={!hasSelection}
220
-            size="small"
221
-            onClick={handleMoveForward}
222
-          >
223
-            <Tooltip label="Move Forward">
224
-              <ArrowUpIcon />
225
-            </Tooltip>
226
-          </IconButton>
227
-
228
-          <IconButton
229
-            bp={breakpoints}
230
-            disabled={!hasSelection}
231
-            size="small"
232
-            onClick={handleMoveToFront}
233
-          >
234
-            <Tooltip label="More to Front">
235
-              <PinTopIcon />
236
-            </Tooltip>
237
-          </IconButton>
238
-
239
-          <IconButton
240
-            bp={breakpoints}
241
-            disabled={!hasSelection}
242
-            size="small"
243
-            onClick={handleDelete}
244
-          >
245
-            <Tooltip label="Delete">
246
-              <Trash2 size="15" />
247
-            </Tooltip>
248
-          </IconButton>
249
-        </ButtonsRow>
82
+        <ShapesFunctions />
250 83
         <AlignDistribute
251
-          hasTwoOrMore={selectedIds.length > 1}
252
-          hasThreeOrMore={selectedIds.length > 2}
84
+          hasTwoOrMore={selectedShapesCount > 1}
85
+          hasThreeOrMore={selectedShapesCount > 2}
253 86
         />
254 87
       </Content>
255 88
     </Panel.Layout>
@@ -306,16 +139,3 @@ const Row = styled('div', {
306 139
     position: 'relative',
307 140
   },
308 141
 })
309
-
310
-const ButtonsRow = styled('div', {
311
-  position: 'relative',
312
-  display: 'flex',
313
-  width: '100%',
314
-  background: 'none',
315
-  border: 'none',
316
-  cursor: 'pointer',
317
-  outline: 'none',
318
-  alignItems: 'center',
319
-  justifyContent: 'flex-start',
320
-  padding: 4,
321
-})

+ 3
- 1
hooks/usePageShapes.ts 查看文件

@@ -26,7 +26,7 @@ export default function usePageShapes(): string[] {
26 26
   }, [])
27 27
 
28 28
   // Get the shapes that fit into the current window
29
-  return useSelector((s) => {
29
+  const visiblePageShapeIds = useSelector((s) => {
30 30
     const pageState = getPageState(s.data)
31 31
 
32 32
     if (!viewportCache.has(pageState)) {
@@ -46,4 +46,6 @@ export default function usePageShapes(): string[] {
46 46
       })
47 47
       .map((shape) => shape.id)
48 48
   }, deepCompareArrays)
49
+
50
+  return visiblePageShapeIds
49 51
 }

+ 9
- 9
state/state.ts 查看文件

@@ -1870,9 +1870,17 @@ const state = createState({
1870 1870
       data.boundsRotation = 0
1871 1871
     },
1872 1872
   },
1873
+  asyncs: {
1874
+    async getUpdatedShapes(data) {
1875
+      return updateFromCode(
1876
+        data,
1877
+        data.document.code[data.currentCodeFileId].code
1878
+      )
1879
+    },
1880
+  },
1873 1881
   values: {
1874 1882
     selectedIds(data) {
1875
-      return new Set(getSelectedIds(data))
1883
+      return setToArray(getSelectedIds(data))
1876 1884
     },
1877 1885
     selectedBounds(data) {
1878 1886
       return getSelectionBounds(data)
@@ -1915,14 +1923,6 @@ const state = createState({
1915 1923
       return commonStyle
1916 1924
     },
1917 1925
   },
1918
-  asyncs: {
1919
-    async getUpdatedShapes(data) {
1920
-      return updateFromCode(
1921
-        data,
1922
-        data.document.code[data.currentCodeFileId].code
1923
-      )
1924
-    },
1925
-  },
1926 1926
 })
1927 1927
 
1928 1928
 export default state

正在加载...
取消
保存