Browse Source

perf improvements around selected / hovered shapes

main
Steve Ruiz 3 years ago
parent
commit
8ff8b87a9e

+ 7
- 12
components/canvas/bounds/bounding-box.tsx View File

2
 import { Edge, Corner } from 'types'
2
 import { Edge, Corner } from 'types'
3
 import { useSelector } from 'state'
3
 import { useSelector } from 'state'
4
 import {
4
 import {
5
-  deepCompareArrays,
6
   getBoundsCenter,
5
   getBoundsCenter,
7
   getCurrentCamera,
6
   getCurrentCamera,
8
   getPage,
7
   getPage,
9
-  getSelectedIds,
10
   getSelectedShapes,
8
   getSelectedShapes,
11
   isMobile,
9
   isMobile,
12
 } from 'utils'
10
 } from 'utils'
24
 
22
 
25
   const bounds = useSelector((s) => s.values.selectedBounds)
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
   const isAllLocked = useSelector((s) => {
31
   const isAllLocked = useSelector((s) => {
37
     const page = getPage(s.data)
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
   const isSingleHandles = useSelector((s) => {
36
   const isSingleHandles = useSelector((s) => {
42
     const page = getPage(s.data)
37
     const page = getPage(s.data)
43
     return (
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 View File

2
 import state, { useSelector } from 'state'
2
 import state, { useSelector } from 'state'
3
 import inputs from 'state/inputs'
3
 import inputs from 'state/inputs'
4
 import styled from 'styles'
4
 import styled from 'styles'
5
-import { deepCompareArrays, getPage } from 'utils'
5
+import { getPage } from 'utils'
6
 
6
 
7
 function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
7
 function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
8
   if (!inputs.canAccept(e.pointerId)) return
8
   if (!inputs.canAccept(e.pointerId)) return
31
 
31
 
32
   const isSelecting = useSelector((s) => s.isIn('selecting'))
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
   const rotation = useSelector((s) => {
34
   const rotation = useSelector((s) => {
35
+    const selectedIds = s.values.selectedIds
36
+
40
     if (selectedIds.length === 1) {
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
     } else {
42
     } else {
45
       return 0
43
       return 0
46
     }
44
     }
47
   })
45
   })
48
 
46
 
49
   const isAllHandles = useSelector((s) => {
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
   if (isAllHandles) return null
60
   if (isAllHandles) return null

+ 4
- 8
components/canvas/bounds/handles.tsx View File

3
 import { useRef } from 'react'
3
 import { useRef } from 'react'
4
 import { useSelector } from 'state'
4
 import { useSelector } from 'state'
5
 import styled from 'styles'
5
 import styled from 'styles'
6
-import { deepCompareArrays, getPage } from 'utils'
6
+import { getPage } from 'utils'
7
 import vec from 'utils/vec'
7
 import vec from 'utils/vec'
8
 
8
 
9
 export default function Handles(): JSX.Element {
9
 export default function Handles(): JSX.Element {
10
-  const selectedIds = useSelector(
11
-    (s) => Array.from(s.values.selectedIds.values()),
12
-    deepCompareArrays
13
-  )
14
-
15
   const shape = useSelector(
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
   const isSelecting = useSelector((s) =>
16
   const isSelecting = useSelector((s) =>

+ 5
- 6
components/canvas/canvas.tsx View File

13
 import useCanvasEvents from 'hooks/useCanvasEvents'
13
 import useCanvasEvents from 'hooks/useCanvasEvents'
14
 import ContextMenu from './context-menu/context-menu'
14
 import ContextMenu from './context-menu/context-menu'
15
 
15
 
16
+function resetError() {
17
+  null
18
+}
19
+
16
 export default function Canvas(): JSX.Element {
20
 export default function Canvas(): JSX.Element {
17
   const rCanvas = useRef<SVGSVGElement>(null)
21
   const rCanvas = useRef<SVGSVGElement>(null)
18
   const rGroup = useRef<SVGGElement>(null)
22
   const rGroup = useRef<SVGGElement>(null)
28
   return (
32
   return (
29
     <ContextMenu>
33
     <ContextMenu>
30
       <MainSVG ref={rCanvas} {...events}>
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
           <Defs />
36
           <Defs />
38
           {isReady && (
37
           {isReady && (
39
             <g ref={rGroup} id="shapes">
38
             <g ref={rGroup} id="shapes">

+ 2
- 9
components/canvas/context-menu/context-menu.tsx View File

5
   IconButton as _IconButton,
5
   IconButton as _IconButton,
6
   RowButton,
6
   RowButton,
7
 } from 'components/shared'
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
 import state, { useSelector } from 'state'
9
 import state, { useSelector } from 'state'
17
 import {
10
 import {
18
   AlignType,
11
   AlignType,
82
   children: React.ReactNode
75
   children: React.ReactNode
83
 }): JSX.Element {
76
 }): JSX.Element {
84
   const selectedShapeIds = useSelector(
77
   const selectedShapeIds = useSelector(
85
-    (s) => setToArray(getSelectedIds(s.data)),
78
+    (s) => s.values.selectedIds,
86
     deepCompareArrays
79
     deepCompareArrays
87
   )
80
   )
88
 
81
 

+ 13
- 9
components/canvas/defs.tsx View File

1
 import { getShapeStyle } from 'state/shape-styles'
1
 import { getShapeStyle } from 'state/shape-styles'
2
 import { getShapeUtils } from 'state/shape-utils'
2
 import { getShapeUtils } from 'state/shape-utils'
3
-import React, { memo } from 'react'
3
+import React from 'react'
4
 import { useSelector } from 'state'
4
 import { useSelector } from 'state'
5
 import { getCurrentCamera } from 'utils'
5
 import { getCurrentCamera } from 'utils'
6
 import { DotCircle, Handle } from './misc'
6
 import { DotCircle, Handle } from './misc'
12
 
12
 
13
   return (
13
   return (
14
     <defs>
14
     <defs>
15
-      {shapeIdsToRender.map((id) => (
16
-        <Def key={id} id={id} />
17
-      ))}
18
       <DotCircle id="dot" r={4} />
15
       <DotCircle id="dot" r={4} />
19
       <Handle id="handle" r={4} />
16
       <Handle id="handle" r={4} />
20
       <ExpandDef />
17
       <ExpandDef />
18
+      {shapeIdsToRender.map((id) => (
19
+        <Def key={id} id={id} />
20
+      ))}
21
     </defs>
21
     </defs>
22
   )
22
   )
23
 }
23
 }
24
 
24
 
25
-const Def = memo(function Def({ id }: { id: string }) {
25
+function Def({ id }: { id: string }) {
26
   const shape = useShapeDef(id)
26
   const shape = useShapeDef(id)
27
 
27
 
28
   if (!shape) return null
28
   if (!shape) return null
29
 
29
 
30
   const style = getShapeStyle(shape.style)
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
 function ExpandDef() {
42
 function ExpandDef() {
39
   const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
43
   const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)

+ 43
- 0
components/canvas/hovered-shape.tsx View File

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 View File

1
 import { useSelector } from 'state'
1
 import { useSelector } from 'state'
2
 import Shape from './shape'
2
 import Shape from './shape'
3
+import HoveredShape from './hovered-shape'
3
 import usePageShapes from 'hooks/usePageShapes'
4
 import usePageShapes from 'hooks/usePageShapes'
4
 
5
 
5
 /* 
6
 /* 
8
 here; and still cheaper than any other pattern I've found.
9
 here; and still cheaper than any other pattern I've found.
9
 */
10
 */
10
 
11
 
11
-const noOffset = [0, 0]
12
-
13
 export default function Page(): JSX.Element {
12
 export default function Page(): JSX.Element {
14
-  const currentPageShapeIds = usePageShapes()
15
-
16
   const isSelecting = useSelector((s) => s.isIn('selecting'))
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
   return (
21
   return (
19
     <g pointerEvents={isSelecting ? 'all' : 'none'}>
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
     </g>
29
     </g>
29
   )
30
   )

+ 2
- 2
components/canvas/selected.tsx View File

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

+ 123
- 159
components/canvas/shape.tsx View File

2
 import { useSelector } from 'state'
2
 import { useSelector } from 'state'
3
 import styled from 'styles'
3
 import styled from 'styles'
4
 import { getShapeUtils } from 'state/shape-utils'
4
 import { getShapeUtils } from 'state/shape-utils'
5
-import { getPage, getSelectedIds, isMobile } from 'utils'
5
+import { deepCompareArrays, getPage, getShape } from 'utils'
6
 import useShapeEvents from 'hooks/useShapeEvents'
6
 import useShapeEvents from 'hooks/useShapeEvents'
7
-import { Shape as _Shape } from 'types'
8
 import vec from 'utils/vec'
7
 import vec from 'utils/vec'
9
 import { getShapeStyle } from 'state/shape-styles'
8
 import { getShapeStyle } from 'state/shape-styles'
10
-
11
-const isMobileDevice = isMobile()
9
+import useShapeDef from 'hooks/useShape'
12
 
10
 
13
 interface ShapeProps {
11
 interface ShapeProps {
14
   id: string
12
   id: string
15
   isSelecting: boolean
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
   const rGroup = useRef<SVGGElement>(null)
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
   return (
59
   return (
63
     <StyledGroup
60
     <StyledGroup
64
       id={id + '-group'}
61
       id={id + '-group'}
65
       ref={rGroup}
62
       ref={rGroup}
66
       transform={transform}
63
       transform={transform}
67
-      isSelected={isSelected}
68
-      device={isMobileDevice ? 'mobile' : 'desktop'}
69
       {...events}
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
         (isForeignObject ? (
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
       {isParent &&
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
     </StyledGroup>
89
     </StyledGroup>
109
   )
90
   )
111
 
92
 
112
 interface RealShapeProps {
93
 interface RealShapeProps {
113
   id: string
94
   id: string
114
-  shape: _Shape
115
   isParent: boolean
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
 const StyledShape = styled('path', {
163
 const StyledShape = styled('path', {
125
   pointerEvents: 'none',
166
   pointerEvents: 'none',
126
 })
167
 })
127
 
168
 
128
-const HoverIndicator = styled('path', {
129
-  stroke: '$selected',
169
+const EventSoak = styled('use', {
170
+  opacity: 0,
130
   strokeLinecap: 'round',
171
   strokeLinecap: 'round',
131
   strokeLinejoin: 'round',
172
   strokeLinejoin: 'round',
132
-  fill: 'transparent',
133
-  filter: 'url(#expand)',
134
   variants: {
173
   variants: {
135
     variant: {
174
     variant: {
136
       ghost: {
175
       ghost: {
150
 
189
 
151
 const StyledGroup = styled('g', {
190
 const StyledGroup = styled('g', {
152
   outline: 'none',
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
 export default memo(Shape)
194
 export default memo(Shape)

+ 14
- 14
components/controls-panel/controls-panel.tsx View File

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

+ 5
- 9
components/page-panel/page-panel.tsx View File

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

+ 7
- 0
components/shared.tsx View File

3
 import * as Panel from './panel'
3
 import * as Panel from './panel'
4
 import styled from 'styles'
4
 import styled from 'styles'
5
 
5
 
6
+export const breakpoints: any = { '@initial': 'mobile', '@sm': 'small' }
7
+
6
 export const IconButton = styled('button', {
8
 export const IconButton = styled('button', {
7
   height: '32px',
9
   height: '32px',
8
   width: '32px',
10
   width: '32px',
124
         width: 'auto',
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 View File

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

+ 13
- 6
components/style-panel/color-content.tsx View File

4
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
4
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
5
 import { Square } from 'react-feather'
5
 import { Square } from 'react-feather'
6
 import { DropdownContent } from '../shared'
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
   return (
17
   return (
14
     <DropdownContent sideOffset={8} side="bottom">
18
     <DropdownContent sideOffset={8} side="bottom">
15
       {Object.keys(strokes).map((color: ColorStyle) => (
19
       {Object.keys(strokes).map((color: ColorStyle) => (
17
           as={IconButton}
21
           as={IconButton}
18
           key={color}
22
           key={color}
19
           title={color}
23
           title={color}
20
-          onSelect={() => onChange(color)}
24
+          value={color}
25
+          onSelect={handleColorChange}
21
         >
26
         >
22
           <Square fill={strokes[color]} stroke="none" size="22" />
27
           <Square fill={strokes[color]} stroke="none" size="22" />
23
         </DropdownMenu.DropdownMenuItem>
28
         </DropdownMenu.DropdownMenuItem>
25
     </DropdownContent>
30
     </DropdownContent>
26
   )
31
   )
27
 }
32
 }
33
+
34
+export default memo(ColorContent)

+ 9
- 12
components/style-panel/color-picker.tsx View File

1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
 import { strokes } from 'state/shape-styles'
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
 import { Square } from 'react-feather'
4
 import { Square } from 'react-feather'
6
 import ColorContent from './color-content'
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
   return (
12
   return (
15
     <DropdownMenu.Root dir="ltr">
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
         <label htmlFor="color">Color</label>
15
         <label htmlFor="color">Color</label>
21
         <IconWrapper>
16
         <IconWrapper>
22
           <Square fill={strokes[color]} />
17
           <Square fill={strokes[color]} />
23
         </IconWrapper>
18
         </IconWrapper>
24
       </DropdownMenu.Trigger>
19
       </DropdownMenu.Trigger>
25
-      <ColorContent onChange={onChange} />
20
+      <ColorContent />
26
     </DropdownMenu.Root>
21
     </DropdownMenu.Root>
27
   )
22
   )
28
 }
23
 }
24
+
25
+export default memo(ColorPicker)

+ 21
- 25
components/style-panel/dash-picker.tsx View File

7
 } from '../shared'
7
 } from '../shared'
8
 import * as RadioGroup from '@radix-ui/react-radio-group'
8
 import * as RadioGroup from '@radix-ui/react-radio-group'
9
 import { DashStyle } from 'types'
9
 import { DashStyle } from 'types'
10
-import state from 'state'
10
+import state, { useSelector } from 'state'
11
+import { memo } from 'react'
11
 
12
 
12
 function handleChange(dash: string) {
13
 function handleChange(dash: string) {
13
   state.send('CHANGED_STYLE', { dash })
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
   return (
26
   return (
22
     <Group name="Dash" onValueChange={handleChange}>
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
     </Group>
38
     </Group>
45
   )
39
   )
46
 }
40
 }
41
+
42
+export default memo(DashPicker)

+ 9
- 10
components/style-panel/is-filled-picker.tsx View File

2
 import { CheckIcon } from '@radix-ui/react-icons'
2
 import { CheckIcon } from '@radix-ui/react-icons'
3
 import { strokes } from 'state/shape-styles'
3
 import { strokes } from 'state/shape-styles'
4
 import { Square } from 'react-feather'
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
   return (
15
   return (
17
     <Checkbox.Root
16
     <Checkbox.Root
18
       dir="ltr"
17
       dir="ltr"
19
       as={RowButton}
18
       as={RowButton}
20
-      bp={{ '@initial': 'mobile', '@sm': 'small' }}
19
+      bp={breakpoints}
21
       checked={isFilled}
20
       checked={isFilled}
22
-      onCheckedChange={onChange}
21
+      onCheckedChange={handleIsFilledChange}
23
     >
22
     >
24
       <label htmlFor="fill">Fill</label>
23
       <label htmlFor="fill">Fill</label>
25
       <IconWrapper>
24
       <IconWrapper>

+ 4
- 9
components/style-panel/quick-color-select.tsx View File

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

+ 22
- 26
components/style-panel/quick-dash-select.tsx View File

1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
-import { IconButton } from 'components/shared'
2
+import { breakpoints, IconButton } from 'components/shared'
3
 import Tooltip from 'components/tooltip'
3
 import Tooltip from 'components/tooltip'
4
+import { memo } from 'react'
4
 import state, { useSelector } from 'state'
5
 import state, { useSelector } from 'state'
5
 import { DashStyle } from 'types'
6
 import { DashStyle } from 'types'
6
 import {
7
 import {
17
   [DashStyle.Dotted]: <DashDottedIcon />,
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
   const dash = useSelector((s) => s.values.selectedStyle.dash)
28
   const dash = useSelector((s) => s.values.selectedStyle.dash)
22
 
29
 
23
   return (
30
   return (
24
     <DropdownMenu.Root dir="ltr">
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
         <Tooltip label="Dash">{dashes[dash]}</Tooltip>
33
         <Tooltip label="Dash">{dashes[dash]}</Tooltip>
30
       </DropdownMenu.Trigger>
34
       </DropdownMenu.Trigger>
31
       <DropdownContent sideOffset={8} direction="vertical">
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
       </DropdownContent>
47
       </DropdownContent>
42
     </DropdownMenu.Root>
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 View File

1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
-import { IconButton } from 'components/shared'
2
+import { breakpoints, IconButton } from 'components/shared'
3
 import Tooltip from 'components/tooltip'
3
 import Tooltip from 'components/tooltip'
4
+import { memo } from 'react'
4
 import { Circle } from 'react-feather'
5
 import { Circle } from 'react-feather'
5
 import state, { useSelector } from 'state'
6
 import state, { useSelector } from 'state'
6
 import { SizeStyle } from 'types'
7
 import { SizeStyle } from 'types'
12
   [SizeStyle.Large]: 22,
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
   const size = useSelector((s) => s.values.selectedStyle.size)
23
   const size = useSelector((s) => s.values.selectedStyle.size)
17
 
24
 
18
   return (
25
   return (
19
     <DropdownMenu.Root dir="ltr">
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
         <Tooltip label="Size">
28
         <Tooltip label="Size">
25
           <Circle size={sizes[size]} stroke="none" fill="currentColor" />
29
           <Circle size={sizes[size]} stroke="none" fill="currentColor" />
26
         </Tooltip>
30
         </Tooltip>
27
       </DropdownMenu.Trigger>
31
       </DropdownMenu.Trigger>
28
       <DropdownContent sideOffset={8} direction="vertical">
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
       </DropdownContent>
44
       </DropdownContent>
36
     </DropdownMenu.Root>
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 View File

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 View File

1
 import { Group, Item } from '../shared'
1
 import { Group, Item } from '../shared'
2
 import * as RadioGroup from '@radix-ui/react-radio-group'
2
 import * as RadioGroup from '@radix-ui/react-radio-group'
3
 import { Circle } from 'react-feather'
3
 import { Circle } from 'react-feather'
4
-import state from 'state'
4
+import state, { useSelector } from 'state'
5
 import { SizeStyle } from 'types'
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
 function handleChange(size: string) {
14
 function handleChange(size: string) {
8
   state.send('CHANGED_STYLE', { size })
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
   return (
21
   return (
13
     <Group name="width" onValueChange={handleChange}>
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
     </Group>
33
     </Group>
36
   )
34
   )
37
 }
35
 }
36
+
37
+export default memo(SizePicker)

+ 12
- 192
components/style-panel/style-panel.tsx View File

3
 import * as Panel from 'components/panel'
3
 import * as Panel from 'components/panel'
4
 import { useRef } from 'react'
4
 import { useRef } from 'react'
5
 import { IconButton } from 'components/shared'
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
 import AlignDistribute from './align-distribute'
8
 import AlignDistribute from './align-distribute'
15
-import { MoveType } from 'types'
16
 import SizePicker from './size-picker'
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
 import DashPicker from './dash-picker'
10
 import DashPicker from './dash-picker'
32
 import QuickColorSelect from './quick-color-select'
11
 import QuickColorSelect from './quick-color-select'
33
 import ColorPicker from './color-picker'
12
 import ColorPicker from './color-picker'
37
 import Tooltip from 'components/tooltip'
16
 import Tooltip from 'components/tooltip'
38
 
17
 
39
 const breakpoints = { '@initial': 'mobile', '@sm': 'small' } as any
18
 const breakpoints = { '@initial': 'mobile', '@sm': 'small' } as any
19
+
40
 const handleStylePanelOpen = () => state.send('TOGGLED_STYLE_PANEL_OPEN')
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
 export default function StylePanel(): JSX.Element {
22
 export default function StylePanel(): JSX.Element {
56
   const rContainer = useRef<HTMLDivElement>(null)
23
   const rContainer = useRef<HTMLDivElement>(null)
24
+
57
   const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
25
   const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
58
 
26
 
59
   return (
27
   return (
86
 // track of this data manually within our state.
54
 // track of this data manually within our state.
87
 
55
 
88
 function SelectedShapeStyles(): JSX.Element {
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
   return (
59
   return (
114
     <Panel.Layout>
60
     <Panel.Layout>
123
         </IconButton>
69
         </IconButton>
124
       </Panel.Header>
70
       </Panel.Header>
125
       <Content>
71
       <Content>
126
-        <ColorPicker color={commonStyle.color} onChange={handleColorChange} />
127
-        <IsFilledPicker
128
-          isFilled={commonStyle.isFilled}
129
-          onChange={handleIsFilledChange}
130
-        />
72
+        <ColorPicker />
73
+        <IsFilledPicker />
131
         <Row>
74
         <Row>
132
           <label htmlFor="size">Size</label>
75
           <label htmlFor="size">Size</label>
133
-          <SizePicker size={commonStyle.size} />
76
+          <SizePicker />
134
         </Row>
77
         </Row>
135
         <Row>
78
         <Row>
136
           <label htmlFor="dash">Dash</label>
79
           <label htmlFor="dash">Dash</label>
137
-          <DashPicker dash={commonStyle.dash} />
80
+          <DashPicker />
138
         </Row>
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
         <AlignDistribute
83
         <AlignDistribute
251
-          hasTwoOrMore={selectedIds.length > 1}
252
-          hasThreeOrMore={selectedIds.length > 2}
84
+          hasTwoOrMore={selectedShapesCount > 1}
85
+          hasThreeOrMore={selectedShapesCount > 2}
253
         />
86
         />
254
       </Content>
87
       </Content>
255
     </Panel.Layout>
88
     </Panel.Layout>
306
     position: 'relative',
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 View File

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

+ 9
- 9
state/state.ts View File

1870
       data.boundsRotation = 0
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
   values: {
1881
   values: {
1874
     selectedIds(data) {
1882
     selectedIds(data) {
1875
-      return new Set(getSelectedIds(data))
1883
+      return setToArray(getSelectedIds(data))
1876
     },
1884
     },
1877
     selectedBounds(data) {
1885
     selectedBounds(data) {
1878
       return getSelectionBounds(data)
1886
       return getSelectionBounds(data)
1915
       return commonStyle
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
 export default state
1928
 export default state

Loading…
Cancel
Save