perf improvements around selected / hovered shapes
This commit is contained in:
parent
2cfeea0449
commit
8ff8b87a9e
26 changed files with 643 additions and 612 deletions
|
@ -2,11 +2,9 @@ import * as React from 'react'
|
||||||
import { Edge, Corner } from 'types'
|
import { Edge, Corner } from 'types'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import {
|
import {
|
||||||
deepCompareArrays,
|
|
||||||
getBoundsCenter,
|
getBoundsCenter,
|
||||||
getCurrentCamera,
|
getCurrentCamera,
|
||||||
getPage,
|
getPage,
|
||||||
getSelectedIds,
|
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
isMobile,
|
isMobile,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
|
@ -24,25 +22,22 @@ export default function Bounds(): JSX.Element {
|
||||||
|
|
||||||
const bounds = useSelector((s) => s.values.selectedBounds)
|
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||||
|
|
||||||
const selectedIds = useSelector(
|
const rotation = useSelector((s) =>
|
||||||
(s) => Array.from(s.values.selectedIds.values()),
|
s.values.selectedIds.length === 1
|
||||||
deepCompareArrays
|
? getSelectedShapes(s.data)[0].rotation
|
||||||
)
|
: 0
|
||||||
|
|
||||||
const rotation = useSelector(({ data }) =>
|
|
||||||
getSelectedIds(data).size === 1 ? getSelectedShapes(data)[0].rotation : 0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const isAllLocked = useSelector((s) => {
|
const isAllLocked = useSelector((s) => {
|
||||||
const page = getPage(s.data)
|
const page = getPage(s.data)
|
||||||
return selectedIds.every((id) => page.shapes[id]?.isLocked)
|
return s.values.selectedIds.every((id) => page.shapes[id]?.isLocked)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isSingleHandles = useSelector((s) => {
|
const isSingleHandles = useSelector((s) => {
|
||||||
const page = getPage(s.data)
|
const page = getPage(s.data)
|
||||||
return (
|
return (
|
||||||
selectedIds.length === 1 &&
|
s.values.selectedIds.length === 1 &&
|
||||||
page.shapes[selectedIds[0]]?.handles !== undefined
|
page.shapes[s.values.selectedIds[0]]?.handles !== undefined
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useRef } from 'react'
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import inputs from 'state/inputs'
|
import inputs from 'state/inputs'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { deepCompareArrays, getPage } from 'utils'
|
import { getPage } from 'utils'
|
||||||
|
|
||||||
function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
|
function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
|
@ -31,28 +31,30 @@ export default function BoundsBg(): JSX.Element {
|
||||||
|
|
||||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||||
|
|
||||||
const selectedIds = useSelector(
|
|
||||||
(s) => Array.from(s.values.selectedIds.values()),
|
|
||||||
deepCompareArrays
|
|
||||||
)
|
|
||||||
|
|
||||||
const rotation = useSelector((s) => {
|
const rotation = useSelector((s) => {
|
||||||
|
const selectedIds = s.values.selectedIds
|
||||||
|
|
||||||
if (selectedIds.length === 1) {
|
if (selectedIds.length === 1) {
|
||||||
const { shapes } = getPage(s.data)
|
const selected = selectedIds[0]
|
||||||
const selected = Array.from(s.values.selectedIds.values())[0]
|
const page = getPage(s.data)
|
||||||
return shapes[selected]?.rotation
|
|
||||||
|
return page.shapes[selected]?.rotation
|
||||||
} else {
|
} else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const isAllHandles = useSelector((s) => {
|
const isAllHandles = useSelector((s) => {
|
||||||
const page = getPage(s.data)
|
const selectedIds = s.values.selectedIds
|
||||||
const selectedIds = Array.from(s.values.selectedIds.values())
|
|
||||||
return (
|
if (selectedIds.length === 1) {
|
||||||
selectedIds.length === 1 &&
|
const page = getPage(s.data)
|
||||||
page.shapes[selectedIds[0]]?.handles !== undefined
|
const selected = selectedIds[0]
|
||||||
)
|
|
||||||
|
return (
|
||||||
|
selectedIds.length === 1 && page.shapes[selected]?.handles !== undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isAllHandles) return null
|
if (isAllHandles) return null
|
||||||
|
|
|
@ -3,18 +3,14 @@ import { getShapeUtils } from 'state/shape-utils'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { deepCompareArrays, getPage } from 'utils'
|
import { getPage } from 'utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
|
|
||||||
export default function Handles(): JSX.Element {
|
export default function Handles(): JSX.Element {
|
||||||
const selectedIds = useSelector(
|
|
||||||
(s) => Array.from(s.values.selectedIds.values()),
|
|
||||||
deepCompareArrays
|
|
||||||
)
|
|
||||||
|
|
||||||
const shape = useSelector(
|
const shape = useSelector(
|
||||||
({ data }) =>
|
(s) =>
|
||||||
selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]]
|
s.values.selectedIds.length === 1 &&
|
||||||
|
getPage(s.data).shapes[s.values.selectedIds[0]]
|
||||||
)
|
)
|
||||||
|
|
||||||
const isSelecting = useSelector((s) =>
|
const isSelecting = useSelector((s) =>
|
||||||
|
|
|
@ -13,6 +13,10 @@ import Handles from './bounds/handles'
|
||||||
import useCanvasEvents from 'hooks/useCanvasEvents'
|
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||||
import ContextMenu from './context-menu/context-menu'
|
import ContextMenu from './context-menu/context-menu'
|
||||||
|
|
||||||
|
function resetError() {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
export default function Canvas(): JSX.Element {
|
export default function Canvas(): JSX.Element {
|
||||||
const rCanvas = useRef<SVGSVGElement>(null)
|
const rCanvas = useRef<SVGSVGElement>(null)
|
||||||
const rGroup = useRef<SVGGElement>(null)
|
const rGroup = useRef<SVGGElement>(null)
|
||||||
|
@ -28,12 +32,7 @@ export default function Canvas(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<MainSVG ref={rCanvas} {...events}>
|
<MainSVG ref={rCanvas} {...events}>
|
||||||
<ErrorBoundary
|
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
|
||||||
FallbackComponent={ErrorFallback}
|
|
||||||
onReset={() => {
|
|
||||||
// reset the state of your app so the error doesn't happen again
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Defs />
|
<Defs />
|
||||||
{isReady && (
|
{isReady && (
|
||||||
<g ref={rGroup} id="shapes">
|
<g ref={rGroup} id="shapes">
|
||||||
|
|
|
@ -5,14 +5,7 @@ import {
|
||||||
IconButton as _IconButton,
|
IconButton as _IconButton,
|
||||||
RowButton,
|
RowButton,
|
||||||
} from 'components/shared'
|
} from 'components/shared'
|
||||||
import {
|
import { commandKey, deepCompareArrays, getShape, isMobile } from 'utils'
|
||||||
commandKey,
|
|
||||||
deepCompareArrays,
|
|
||||||
getSelectedIds,
|
|
||||||
getShape,
|
|
||||||
isMobile,
|
|
||||||
setToArray,
|
|
||||||
} from 'utils'
|
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import {
|
import {
|
||||||
AlignType,
|
AlignType,
|
||||||
|
@ -82,7 +75,7 @@ export default function ContextMenu({
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const selectedShapeIds = useSelector(
|
const selectedShapeIds = useSelector(
|
||||||
(s) => setToArray(getSelectedIds(s.data)),
|
(s) => s.values.selectedIds,
|
||||||
deepCompareArrays
|
deepCompareArrays
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { getShapeStyle } from 'state/shape-styles'
|
import { getShapeStyle } from 'state/shape-styles'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
import React, { memo } from 'react'
|
import React from 'react'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import { getCurrentCamera } from 'utils'
|
import { getCurrentCamera } from 'utils'
|
||||||
import { DotCircle, Handle } from './misc'
|
import { DotCircle, Handle } from './misc'
|
||||||
|
@ -12,28 +12,32 @@ export default function Defs(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<defs>
|
<defs>
|
||||||
{shapeIdsToRender.map((id) => (
|
|
||||||
<Def key={id} id={id} />
|
|
||||||
))}
|
|
||||||
<DotCircle id="dot" r={4} />
|
<DotCircle id="dot" r={4} />
|
||||||
<Handle id="handle" r={4} />
|
<Handle id="handle" r={4} />
|
||||||
<ExpandDef />
|
<ExpandDef />
|
||||||
|
{shapeIdsToRender.map((id) => (
|
||||||
|
<Def key={id} id={id} />
|
||||||
|
))}
|
||||||
</defs>
|
</defs>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Def = memo(function Def({ id }: { id: string }) {
|
function Def({ id }: { id: string }) {
|
||||||
const shape = useShapeDef(id)
|
const shape = useShapeDef(id)
|
||||||
|
|
||||||
if (!shape) return null
|
if (!shape) return null
|
||||||
|
|
||||||
const style = getShapeStyle(shape.style)
|
const style = getShapeStyle(shape.style)
|
||||||
|
|
||||||
return React.cloneElement(
|
return (
|
||||||
getShapeUtils(shape).render(shape, { isEditing: false }),
|
<>
|
||||||
{ id, ...style }
|
{React.cloneElement(
|
||||||
|
getShapeUtils(shape).render(shape, { isEditing: false }),
|
||||||
|
{ id, ...style, strokeWidth: undefined }
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
function ExpandDef() {
|
function ExpandDef() {
|
||||||
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
|
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
|
||||||
|
|
43
components/canvas/hovered-shape.tsx
Normal file
43
components/canvas/hovered-shape.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { memo } from 'react'
|
||||||
|
import { getShape } from 'utils'
|
||||||
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
|
import vec from 'utils/vec'
|
||||||
|
import styled from 'styles'
|
||||||
|
import { useSelector } from 'state'
|
||||||
|
import { getShapeStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
|
function HoveredShape({ id }: { id: string }) {
|
||||||
|
const transform = useSelector((s) => {
|
||||||
|
const shape = getShape(s.data, id)
|
||||||
|
const center = getShapeUtils(shape).getCenter(shape)
|
||||||
|
const rotation = shape.rotation * (180 / Math.PI)
|
||||||
|
const parentPoint = getShape(s.data, shape.parentId)?.point || [0, 0]
|
||||||
|
|
||||||
|
return `
|
||||||
|
translate(${vec.neg(parentPoint)})
|
||||||
|
rotate(${rotation}, ${center})
|
||||||
|
translate(${shape.point})
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
const strokeWidth = useSelector((s) => {
|
||||||
|
const shape = getShape(s.data, id)
|
||||||
|
const style = getShapeStyle(shape.style)
|
||||||
|
return +style.strokeWidth
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g transform={transform}>
|
||||||
|
<StyledHoverShape href={'#' + id} strokeWidth={strokeWidth + 8} />
|
||||||
|
<text>hello</text>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledHoverShape = styled('use', {
|
||||||
|
stroke: '$selected',
|
||||||
|
filter: 'url(#expand)',
|
||||||
|
opacity: 0.1,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default memo(HoveredShape)
|
|
@ -1,5 +1,6 @@
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import Shape from './shape'
|
import Shape from './shape'
|
||||||
|
import HoveredShape from './hovered-shape'
|
||||||
import usePageShapes from 'hooks/usePageShapes'
|
import usePageShapes from 'hooks/usePageShapes'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -8,22 +9,22 @@ on the current page. Kind of expensive but only happens
|
||||||
here; and still cheaper than any other pattern I've found.
|
here; and still cheaper than any other pattern I've found.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const noOffset = [0, 0]
|
|
||||||
|
|
||||||
export default function Page(): JSX.Element {
|
export default function Page(): JSX.Element {
|
||||||
const currentPageShapeIds = usePageShapes()
|
|
||||||
|
|
||||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||||
|
|
||||||
|
const visiblePageShapeIds = usePageShapes()
|
||||||
|
|
||||||
|
const hoveredShapeId = useSelector((s) => {
|
||||||
|
return visiblePageShapeIds.find((id) => id === s.data.hoveredId)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g pointerEvents={isSelecting ? 'all' : 'none'}>
|
<g pointerEvents={isSelecting ? 'all' : 'none'}>
|
||||||
{currentPageShapeIds.map((shapeId) => (
|
{isSelecting && hoveredShapeId && (
|
||||||
<Shape
|
<HoveredShape key={hoveredShapeId} id={hoveredShapeId} />
|
||||||
key={shapeId}
|
)}
|
||||||
id={shapeId}
|
{visiblePageShapeIds.map((id) => (
|
||||||
isSelecting={isSelecting}
|
<Shape key={id} id={id} isSelecting={isSelecting} />
|
||||||
parentPoint={noOffset}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import { deepCompareArrays, getPage, getSelectedIds, setToArray } from 'utils'
|
import { deepCompareArrays, getPage } from 'utils'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
|
||||||
export default function Selected(): JSX.Element {
|
export default function Selected(): JSX.Element {
|
||||||
const currentSelectedShapeIds = useSelector(
|
const currentSelectedShapeIds = useSelector(
|
||||||
({ data }) => setToArray(getSelectedIds(data)),
|
(s) => s.values.selectedIds,
|
||||||
deepCompareArrays
|
deepCompareArrays
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,31 +2,150 @@ import React, { useRef, memo, useEffect } from 'react'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
import { getPage, getSelectedIds, isMobile } from 'utils'
|
import { deepCompareArrays, getPage, getShape } from 'utils'
|
||||||
import useShapeEvents from 'hooks/useShapeEvents'
|
import useShapeEvents from 'hooks/useShapeEvents'
|
||||||
import { Shape as _Shape } from 'types'
|
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { getShapeStyle } from 'state/shape-styles'
|
import { getShapeStyle } from 'state/shape-styles'
|
||||||
|
import useShapeDef from 'hooks/useShape'
|
||||||
const isMobileDevice = isMobile()
|
|
||||||
|
|
||||||
interface ShapeProps {
|
interface ShapeProps {
|
||||||
id: string
|
id: string
|
||||||
isSelecting: boolean
|
isSelecting: boolean
|
||||||
parentPoint: number[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Shape({ id, isSelecting, parentPoint }: ShapeProps): JSX.Element {
|
function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
|
||||||
const rGroup = useRef<SVGGElement>(null)
|
const rGroup = useRef<SVGGElement>(null)
|
||||||
|
|
||||||
|
const shapeUtils = useSelector((s) => {
|
||||||
|
const shape = getShape(s.data, id)
|
||||||
|
return getShapeUtils(shape)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isHidden = useSelector((s) => {
|
||||||
|
const shape = getShape(s.data, id)
|
||||||
|
return shape.isHidden
|
||||||
|
})
|
||||||
|
|
||||||
|
const children = useSelector((s) => {
|
||||||
|
const shape = getShape(s.data, id)
|
||||||
|
return shape.children
|
||||||
|
}, deepCompareArrays)
|
||||||
|
|
||||||
|
const isParent = shapeUtils.isParent
|
||||||
|
|
||||||
|
const isForeignObject = shapeUtils.isForeignObject
|
||||||
|
|
||||||
|
const strokeWidth = useSelector((s) => {
|
||||||
|
const shape = getShape(s.data, id)
|
||||||
|
const style = getShapeStyle(shape.style)
|
||||||
|
return +style.strokeWidth
|
||||||
|
})
|
||||||
|
|
||||||
|
const transform = useSelector((s) => {
|
||||||
|
const shape = getShape(s.data, id)
|
||||||
|
const center = shapeUtils.getCenter(shape)
|
||||||
|
const rotation = shape.rotation * (180 / Math.PI)
|
||||||
|
const parentPoint = getShape(s.data, shape.parentId)?.point || [0, 0]
|
||||||
|
|
||||||
|
return `
|
||||||
|
translate(${vec.neg(parentPoint)})
|
||||||
|
rotate(${rotation}, ${center})
|
||||||
|
translate(${shape.point})
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
const events = useShapeEvents(id, isParent, rGroup)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledGroup
|
||||||
|
id={id + '-group'}
|
||||||
|
ref={rGroup}
|
||||||
|
transform={transform}
|
||||||
|
{...events}
|
||||||
|
>
|
||||||
|
{isSelecting &&
|
||||||
|
(isForeignObject ? (
|
||||||
|
<ForeignObjectHover id={id} />
|
||||||
|
) : (
|
||||||
|
<EventSoak
|
||||||
|
as="use"
|
||||||
|
href={'#' + id}
|
||||||
|
strokeWidth={strokeWidth + 8}
|
||||||
|
variant={shapeUtils.canStyleFill ? 'filled' : 'hollow'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!isHidden &&
|
||||||
|
(isForeignObject ? (
|
||||||
|
<ForeignObjectRender id={id} />
|
||||||
|
) : (
|
||||||
|
<RealShape id={id} isParent={isParent} strokeWidth={strokeWidth} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isParent &&
|
||||||
|
children.map((shapeId) => (
|
||||||
|
<Shape key={shapeId} id={shapeId} isSelecting={isSelecting} />
|
||||||
|
))}
|
||||||
|
</StyledGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RealShapeProps {
|
||||||
|
id: string
|
||||||
|
isParent: boolean
|
||||||
|
strokeWidth: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const RealShape = memo(function RealShape({
|
||||||
|
id,
|
||||||
|
isParent,
|
||||||
|
strokeWidth,
|
||||||
|
}: RealShapeProps) {
|
||||||
|
return (
|
||||||
|
<StyledShape
|
||||||
|
as="use"
|
||||||
|
data-shy={isParent}
|
||||||
|
href={'#' + id}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const ForeignObjectHover = memo(function ForeignObjectHover({
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
}) {
|
||||||
|
const size = useSelector((s) => {
|
||||||
|
const shape = getPage(s.data).shapes[id]
|
||||||
|
const bounds = getShapeUtils(shape).getBounds(shape)
|
||||||
|
|
||||||
|
return [bounds.width, bounds.height]
|
||||||
|
}, deepCompareArrays)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventSoak
|
||||||
|
as="rect"
|
||||||
|
width={size[0]}
|
||||||
|
height={size[1]}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
variant={'ghost'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const ForeignObjectRender = memo(function ForeignObjectRender({
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
}) {
|
||||||
|
const shape = useShapeDef(id)
|
||||||
|
|
||||||
const rFocusable = useRef<HTMLTextAreaElement>(null)
|
const rFocusable = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
const isEditing = useSelector((s) => s.data.editingId === id)
|
const isEditing = useSelector((s) => s.data.editingId === id)
|
||||||
|
|
||||||
const isSelected = useSelector((s) => getSelectedIds(s.data).has(id))
|
const shapeUtils = getShapeUtils(shape)
|
||||||
|
|
||||||
const shape = useSelector((s) => getPage(s.data).shapes[id])
|
|
||||||
|
|
||||||
const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
|
@ -38,85 +157,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps): JSX.Element {
|
||||||
}
|
}
|
||||||
}, [isEditing])
|
}, [isEditing])
|
||||||
|
|
||||||
// This is a problem with deleted shapes. The hooks in this component
|
return shapeUtils.render(shape, { isEditing, ref: rFocusable })
|
||||||
// may sometimes run before the hook in the Page component, which means
|
|
||||||
// a deleted shape will still be pulled here before the page component
|
|
||||||
// detects the change and pulls this component.
|
|
||||||
if (!shape) return null
|
|
||||||
|
|
||||||
const style = getShapeStyle(shape.style)
|
|
||||||
const shapeUtils = getShapeUtils(shape)
|
|
||||||
|
|
||||||
const { isShy, isParent, isForeignObject } = shapeUtils
|
|
||||||
|
|
||||||
const bounds = shapeUtils.getBounds(shape)
|
|
||||||
const center = shapeUtils.getCenter(shape)
|
|
||||||
const rotation = shape.rotation * (180 / Math.PI)
|
|
||||||
|
|
||||||
const transform = `
|
|
||||||
translate(${vec.neg(parentPoint)})
|
|
||||||
rotate(${rotation}, ${center})
|
|
||||||
translate(${shape.point})
|
|
||||||
`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledGroup
|
|
||||||
id={id + '-group'}
|
|
||||||
ref={rGroup}
|
|
||||||
transform={transform}
|
|
||||||
isSelected={isSelected}
|
|
||||||
device={isMobileDevice ? 'mobile' : 'desktop'}
|
|
||||||
{...events}
|
|
||||||
>
|
|
||||||
{isSelecting && !isShy && (
|
|
||||||
<>
|
|
||||||
{isForeignObject ? (
|
|
||||||
<HoverIndicator
|
|
||||||
as="rect"
|
|
||||||
width={bounds.width}
|
|
||||||
height={bounds.height}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
variant={'ghost'}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<HoverIndicator
|
|
||||||
as="use"
|
|
||||||
href={'#' + id}
|
|
||||||
strokeWidth={+style.strokeWidth + 5}
|
|
||||||
variant={shapeUtils.canStyleFill ? 'filled' : 'hollow'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!shape.isHidden &&
|
|
||||||
(isForeignObject ? (
|
|
||||||
shapeUtils.render(shape, { isEditing, ref: rFocusable })
|
|
||||||
) : (
|
|
||||||
<RealShape id={id} isParent={isParent} shape={shape} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{isParent &&
|
|
||||||
shape.children.map((shapeId) => (
|
|
||||||
<Shape
|
|
||||||
key={shapeId}
|
|
||||||
id={shapeId}
|
|
||||||
isSelecting={isSelecting}
|
|
||||||
parentPoint={shape.point}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</StyledGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RealShapeProps {
|
|
||||||
id: string
|
|
||||||
shape: _Shape
|
|
||||||
isParent: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const RealShape = memo(function RealShape({ id, isParent }: RealShapeProps) {
|
|
||||||
return <StyledShape as="use" data-shy={isParent} href={'#' + id} />
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const StyledShape = styled('path', {
|
const StyledShape = styled('path', {
|
||||||
|
@ -125,12 +166,10 @@ const StyledShape = styled('path', {
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
})
|
})
|
||||||
|
|
||||||
const HoverIndicator = styled('path', {
|
const EventSoak = styled('use', {
|
||||||
stroke: '$selected',
|
opacity: 0,
|
||||||
strokeLinecap: 'round',
|
strokeLinecap: 'round',
|
||||||
strokeLinejoin: 'round',
|
strokeLinejoin: 'round',
|
||||||
fill: 'transparent',
|
|
||||||
filter: 'url(#expand)',
|
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
ghost: {
|
ghost: {
|
||||||
|
@ -150,81 +189,6 @@ const HoverIndicator = styled('path', {
|
||||||
|
|
||||||
const StyledGroup = styled('g', {
|
const StyledGroup = styled('g', {
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
[`& *[data-shy="true"]`]: {
|
|
||||||
opacity: '0',
|
|
||||||
},
|
|
||||||
[`& ${HoverIndicator}`]: {
|
|
||||||
opacity: '0',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
device: {
|
|
||||||
mobile: {},
|
|
||||||
desktop: {},
|
|
||||||
},
|
|
||||||
isSelected: {
|
|
||||||
true: {
|
|
||||||
[`& *[data-shy="true"]`]: {
|
|
||||||
opacity: '1',
|
|
||||||
},
|
|
||||||
[`& ${HoverIndicator}`]: {
|
|
||||||
opacity: '0.2',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
false: {
|
|
||||||
[`& ${HoverIndicator}`]: {
|
|
||||||
opacity: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
compoundVariants: [
|
|
||||||
{
|
|
||||||
device: 'desktop',
|
|
||||||
isSelected: 'false',
|
|
||||||
css: {
|
|
||||||
[`&:hover ${HoverIndicator}`]: {
|
|
||||||
opacity: '0.16',
|
|
||||||
},
|
|
||||||
[`&:hover *[data-shy="true"]`]: {
|
|
||||||
opacity: '1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
device: 'desktop',
|
|
||||||
isSelected: 'true',
|
|
||||||
css: {
|
|
||||||
[`&:hover ${HoverIndicator}`]: {
|
|
||||||
opacity: '0.25',
|
|
||||||
},
|
|
||||||
[`&:active ${HoverIndicator}`]: {
|
|
||||||
opacity: '0.25',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// function Label({ children }: { children: React.ReactNode }) {
|
|
||||||
// return (
|
|
||||||
// <text
|
|
||||||
// y={4}
|
|
||||||
// x={4}
|
|
||||||
// fontSize={12}
|
|
||||||
// fill="black"
|
|
||||||
// stroke="none"
|
|
||||||
// alignmentBaseline="text-before-edge"
|
|
||||||
// pointerEvents="none"
|
|
||||||
// >
|
|
||||||
// {children}
|
|
||||||
// </text>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function pp(n: number[]) {
|
|
||||||
// return '[' + n.map((v) => v.toFixed(1)).join(', ') + ']'
|
|
||||||
// }
|
|
||||||
|
|
||||||
export { HoverIndicator }
|
|
||||||
|
|
||||||
export default memo(Shape)
|
export default memo(Shape)
|
||||||
|
|
|
@ -3,40 +3,44 @@ import styled from 'styles'
|
||||||
import React, { useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import { X, Code } from 'react-feather'
|
import { X, Code } from 'react-feather'
|
||||||
import { IconButton } from 'components/shared'
|
import { breakpoints, IconButton } from 'components/shared'
|
||||||
import * as Panel from '../panel'
|
import * as Panel from '../panel'
|
||||||
import Control from './control'
|
import Control from './control'
|
||||||
import { deepCompareArrays } from 'utils'
|
import { deepCompareArrays } from 'utils'
|
||||||
|
|
||||||
|
function openCodePanel() {
|
||||||
|
state.send('CLOSED_CODE_PANEL')
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCodePanel() {
|
||||||
|
state.send('OPENED_CODE_PANEL')
|
||||||
|
}
|
||||||
|
|
||||||
const stopKeyboardPropagation = (e: KeyboardEvent | React.KeyboardEvent) =>
|
const stopKeyboardPropagation = (e: KeyboardEvent | React.KeyboardEvent) =>
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
export default function ControlPanel(): JSX.Element {
|
export default function ControlPanel(): JSX.Element {
|
||||||
const rContainer = useRef<HTMLDivElement>(null)
|
const rContainer = useRef<HTMLDivElement>(null)
|
||||||
|
const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0)
|
||||||
const codeControls = useSelector(
|
const codeControls = useSelector(
|
||||||
(state) => Object.keys(state.data.codeControls),
|
(state) => Object.keys(state.data.codeControls),
|
||||||
deepCompareArrays
|
deepCompareArrays
|
||||||
)
|
)
|
||||||
const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Root
|
<Panel.Root
|
||||||
|
ref={rContainer}
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
data-bp-desktop
|
data-bp-desktop
|
||||||
ref={rContainer}
|
|
||||||
isOpen={isOpen}
|
|
||||||
variant="controls"
|
variant="controls"
|
||||||
|
isOpen={isOpen}
|
||||||
onKeyDown={stopKeyboardPropagation}
|
onKeyDown={stopKeyboardPropagation}
|
||||||
onKeyUp={stopKeyboardPropagation}
|
onKeyUp={stopKeyboardPropagation}
|
||||||
>
|
>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<Panel.Layout>
|
<Panel.Layout>
|
||||||
<Panel.Header>
|
<Panel.Header>
|
||||||
<IconButton
|
<IconButton bp={breakpoints} size="small" onClick={closeCodePanel}>
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
|
||||||
size="small"
|
|
||||||
onClick={() => state.send('CLOSED_CODE_PANEL')}
|
|
||||||
>
|
|
||||||
<X />
|
<X />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<h3>Controls</h3>
|
<h3>Controls</h3>
|
||||||
|
@ -48,11 +52,7 @@ export default function ControlPanel(): JSX.Element {
|
||||||
</ControlsList>
|
</ControlsList>
|
||||||
</Panel.Layout>
|
</Panel.Layout>
|
||||||
) : (
|
) : (
|
||||||
<IconButton
|
<IconButton bp={breakpoints} size="small" onClick={openCodePanel}>
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
|
||||||
size="small"
|
|
||||||
onClick={() => state.send('OPENED_CODE_PANEL')}
|
|
||||||
>
|
|
||||||
<Code />
|
<Code />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import styled from 'styles'
|
||||||
import * as ContextMenu from '@radix-ui/react-context-menu'
|
import * as ContextMenu from '@radix-ui/react-context-menu'
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
|
|
||||||
import { IconWrapper, RowButton } from 'components/shared'
|
import { breakpoints, IconWrapper, RowButton } from 'components/shared'
|
||||||
import { CheckIcon, PlusIcon } from '@radix-ui/react-icons'
|
import { CheckIcon, PlusIcon } from '@radix-ui/react-icons'
|
||||||
import * as Panel from '../panel'
|
import * as Panel from '../panel'
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
|
@ -40,8 +40,8 @@ export default function PagePanel(): JSX.Element {
|
||||||
<PanelRoot dir="ltr">
|
<PanelRoot dir="ltr">
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
as={RowButton}
|
as={RowButton}
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
css={{ paddingRight: 12 }}
|
variant="pageButton"
|
||||||
>
|
>
|
||||||
<span>{documentPages[currentPageId].name}</span>
|
<span>{documentPages[currentPageId].name}</span>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
@ -58,11 +58,7 @@ export default function PagePanel(): JSX.Element {
|
||||||
{sorted.map(({ id, name }) => (
|
{sorted.map(({ id, name }) => (
|
||||||
<ContextMenu.Root dir="ltr" key={id}>
|
<ContextMenu.Root dir="ltr" key={id}>
|
||||||
<ContextMenu.Trigger>
|
<ContextMenu.Trigger>
|
||||||
<StyledRadioItem
|
<StyledRadioItem key={id} value={id} bp={breakpoints}>
|
||||||
key={id}
|
|
||||||
value={id}
|
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
|
||||||
>
|
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
<DropdownMenu.ItemIndicator as={IconWrapper} size="small">
|
<DropdownMenu.ItemIndicator as={IconWrapper} size="small">
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
|
@ -91,7 +87,7 @@ export default function PagePanel(): JSX.Element {
|
||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<RowButton
|
<RowButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
state.send('CREATED_PAGE')
|
state.send('CREATED_PAGE')
|
||||||
|
|
|
@ -3,6 +3,8 @@ import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||||
import * as Panel from './panel'
|
import * as Panel from './panel'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
|
|
||||||
|
export const breakpoints: any = { '@initial': 'mobile', '@sm': 'small' }
|
||||||
|
|
||||||
export const IconButton = styled('button', {
|
export const IconButton = styled('button', {
|
||||||
height: '32px',
|
height: '32px',
|
||||||
width: '32px',
|
width: '32px',
|
||||||
|
@ -124,6 +126,11 @@ export const RowButton = styled('button', {
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
variant: {
|
||||||
|
pageButton: {
|
||||||
|
paddingRight: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ import {
|
||||||
StretchHorizontallyIcon,
|
StretchHorizontallyIcon,
|
||||||
StretchVerticallyIcon,
|
StretchVerticallyIcon,
|
||||||
} from '@radix-ui/react-icons'
|
} from '@radix-ui/react-icons'
|
||||||
import { IconButton } from 'components/shared'
|
import { breakpoints, IconButton } from 'components/shared'
|
||||||
|
import { memo } from 'react'
|
||||||
import state from 'state'
|
import state from 'state'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { AlignType, DistributeType, StretchType } from 'types'
|
import { AlignType, DistributeType, StretchType } from 'types'
|
||||||
|
@ -55,7 +56,7 @@ function distributeHorizontally() {
|
||||||
state.send('DISTRIBUTED', { type: DistributeType.Horizontal })
|
state.send('DISTRIBUTED', { type: DistributeType.Horizontal })
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AlignDistribute({
|
function AlignDistribute({
|
||||||
hasTwoOrMore,
|
hasTwoOrMore,
|
||||||
hasThreeOrMore,
|
hasThreeOrMore,
|
||||||
}: {
|
}: {
|
||||||
|
@ -65,7 +66,7 @@ export default function AlignDistribute({
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasTwoOrMore}
|
disabled={!hasTwoOrMore}
|
||||||
onClick={alignLeft}
|
onClick={alignLeft}
|
||||||
|
@ -73,7 +74,7 @@ export default function AlignDistribute({
|
||||||
<AlignLeftIcon />
|
<AlignLeftIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasTwoOrMore}
|
disabled={!hasTwoOrMore}
|
||||||
onClick={alignCenterHorizontal}
|
onClick={alignCenterHorizontal}
|
||||||
|
@ -81,7 +82,7 @@ export default function AlignDistribute({
|
||||||
<AlignCenterHorizontallyIcon />
|
<AlignCenterHorizontallyIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasTwoOrMore}
|
disabled={!hasTwoOrMore}
|
||||||
onClick={alignRight}
|
onClick={alignRight}
|
||||||
|
@ -89,7 +90,7 @@ export default function AlignDistribute({
|
||||||
<AlignRightIcon />
|
<AlignRightIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasTwoOrMore}
|
disabled={!hasTwoOrMore}
|
||||||
onClick={stretchHorizontally}
|
onClick={stretchHorizontally}
|
||||||
|
@ -97,7 +98,7 @@ export default function AlignDistribute({
|
||||||
<StretchHorizontallyIcon />
|
<StretchHorizontallyIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasThreeOrMore}
|
disabled={!hasThreeOrMore}
|
||||||
onClick={distributeHorizontally}
|
onClick={distributeHorizontally}
|
||||||
|
@ -105,7 +106,7 @@ export default function AlignDistribute({
|
||||||
<SpaceEvenlyHorizontallyIcon />
|
<SpaceEvenlyHorizontallyIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasTwoOrMore}
|
disabled={!hasTwoOrMore}
|
||||||
onClick={alignTop}
|
onClick={alignTop}
|
||||||
|
@ -113,7 +114,7 @@ export default function AlignDistribute({
|
||||||
<AlignTopIcon />
|
<AlignTopIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasTwoOrMore}
|
disabled={!hasTwoOrMore}
|
||||||
onClick={alignCenterVertical}
|
onClick={alignCenterVertical}
|
||||||
|
@ -121,7 +122,7 @@ export default function AlignDistribute({
|
||||||
<AlignCenterVerticallyIcon />
|
<AlignCenterVerticallyIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasTwoOrMore}
|
disabled={!hasTwoOrMore}
|
||||||
onClick={alignBottom}
|
onClick={alignBottom}
|
||||||
|
@ -129,7 +130,7 @@ export default function AlignDistribute({
|
||||||
<AlignBottomIcon />
|
<AlignBottomIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasTwoOrMore}
|
disabled={!hasTwoOrMore}
|
||||||
onClick={stretchVertically}
|
onClick={stretchVertically}
|
||||||
|
@ -137,7 +138,7 @@ export default function AlignDistribute({
|
||||||
<StretchVerticallyIcon />
|
<StretchVerticallyIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!hasThreeOrMore}
|
disabled={!hasThreeOrMore}
|
||||||
onClick={distributeVertically}
|
onClick={distributeVertically}
|
||||||
|
@ -148,6 +149,8 @@ export default function AlignDistribute({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(AlignDistribute)
|
||||||
|
|
||||||
const Container = styled('div', {
|
const Container = styled('div', {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
padding: 4,
|
padding: 4,
|
||||||
|
|
|
@ -4,12 +4,16 @@ import { ColorStyle } from 'types'
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { Square } from 'react-feather'
|
import { Square } from 'react-feather'
|
||||||
import { DropdownContent } from '../shared'
|
import { DropdownContent } from '../shared'
|
||||||
|
import { memo } from 'react'
|
||||||
|
import state from 'state'
|
||||||
|
|
||||||
export default function ColorContent({
|
function handleColorChange(
|
||||||
onChange,
|
e: Event & { currentTarget: { value: ColorStyle } }
|
||||||
}: {
|
): void {
|
||||||
onChange: (color: ColorStyle) => void
|
state.send('CHANGED_STYLE', { color: e.currentTarget.value })
|
||||||
}): JSX.Element {
|
}
|
||||||
|
|
||||||
|
function ColorContent(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DropdownContent sideOffset={8} side="bottom">
|
<DropdownContent sideOffset={8} side="bottom">
|
||||||
{Object.keys(strokes).map((color: ColorStyle) => (
|
{Object.keys(strokes).map((color: ColorStyle) => (
|
||||||
|
@ -17,7 +21,8 @@ export default function ColorContent({
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
key={color}
|
key={color}
|
||||||
title={color}
|
title={color}
|
||||||
onSelect={() => onChange(color)}
|
value={color}
|
||||||
|
onSelect={handleColorChange}
|
||||||
>
|
>
|
||||||
<Square fill={strokes[color]} stroke="none" size="22" />
|
<Square fill={strokes[color]} stroke="none" size="22" />
|
||||||
</DropdownMenu.DropdownMenuItem>
|
</DropdownMenu.DropdownMenuItem>
|
||||||
|
@ -25,3 +30,5 @@ export default function ColorContent({
|
||||||
</DropdownContent>
|
</DropdownContent>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(ColorContent)
|
||||||
|
|
|
@ -1,28 +1,25 @@
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { strokes } from 'state/shape-styles'
|
import { strokes } from 'state/shape-styles'
|
||||||
import { ColorStyle } from 'types'
|
import { RowButton, IconWrapper, breakpoints } from '../shared'
|
||||||
import { RowButton, IconWrapper } from '../shared'
|
|
||||||
import { Square } from 'react-feather'
|
import { Square } from 'react-feather'
|
||||||
import ColorContent from './color-content'
|
import ColorContent from './color-content'
|
||||||
|
import { memo } from 'react'
|
||||||
|
import { useSelector } from 'state'
|
||||||
|
|
||||||
interface Props {
|
function ColorPicker(): JSX.Element {
|
||||||
color: ColorStyle
|
const color = useSelector((s) => s.values.selectedStyle.color)
|
||||||
onChange: (color: ColorStyle) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ColorPicker({ color, onChange }: Props): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root dir="ltr">
|
<DropdownMenu.Root dir="ltr">
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger as={RowButton} bp={breakpoints}>
|
||||||
as={RowButton}
|
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
|
||||||
>
|
|
||||||
<label htmlFor="color">Color</label>
|
<label htmlFor="color">Color</label>
|
||||||
<IconWrapper>
|
<IconWrapper>
|
||||||
<Square fill={strokes[color]} />
|
<Square fill={strokes[color]} />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<ColorContent onChange={onChange} />
|
<ColorContent />
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(ColorPicker)
|
||||||
|
|
|
@ -7,40 +7,36 @@ import {
|
||||||
} from '../shared'
|
} from '../shared'
|
||||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||||
import { DashStyle } from 'types'
|
import { DashStyle } from 'types'
|
||||||
import state from 'state'
|
import state, { useSelector } from 'state'
|
||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
function handleChange(dash: string) {
|
function handleChange(dash: string) {
|
||||||
state.send('CHANGED_STYLE', { dash })
|
state.send('CHANGED_STYLE', { dash })
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
const dashes = {
|
||||||
dash: DashStyle
|
[DashStyle.Solid]: <DashSolidIcon />,
|
||||||
|
[DashStyle.Dashed]: <DashDashedIcon />,
|
||||||
|
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashPicker({ dash }: Props): JSX.Element {
|
function DashPicker(): JSX.Element {
|
||||||
|
const dash = useSelector((s) => s.values.selectedStyle.dash)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="Dash" onValueChange={handleChange}>
|
<Group name="Dash" onValueChange={handleChange}>
|
||||||
<Item
|
{Object.keys(DashStyle).map((dashStyle: DashStyle) => (
|
||||||
as={RadioGroup.RadioGroupItem}
|
<RadioGroup.RadioGroupItem
|
||||||
value={DashStyle.Solid}
|
as={Item}
|
||||||
isActive={dash === DashStyle.Solid}
|
key={dashStyle}
|
||||||
>
|
isActive={dash === dashStyle}
|
||||||
<DashSolidIcon />
|
value={dashStyle}
|
||||||
</Item>
|
>
|
||||||
<Item
|
{dashes[dashStyle]}
|
||||||
as={RadioGroup.RadioGroupItem}
|
</RadioGroup.RadioGroupItem>
|
||||||
value={DashStyle.Dashed}
|
))}
|
||||||
isActive={dash === DashStyle.Dashed}
|
|
||||||
>
|
|
||||||
<DashDashedIcon />
|
|
||||||
</Item>
|
|
||||||
<Item
|
|
||||||
as={RadioGroup.RadioGroupItem}
|
|
||||||
value={DashStyle.Dotted}
|
|
||||||
isActive={dash === DashStyle.Dotted}
|
|
||||||
>
|
|
||||||
<DashDottedIcon />
|
|
||||||
</Item>
|
|
||||||
</Group>
|
</Group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(DashPicker)
|
||||||
|
|
|
@ -2,24 +2,23 @@ import * as Checkbox from '@radix-ui/react-checkbox'
|
||||||
import { CheckIcon } from '@radix-ui/react-icons'
|
import { CheckIcon } from '@radix-ui/react-icons'
|
||||||
import { strokes } from 'state/shape-styles'
|
import { strokes } from 'state/shape-styles'
|
||||||
import { Square } from 'react-feather'
|
import { Square } from 'react-feather'
|
||||||
import { IconWrapper, RowButton } from '../shared'
|
import { breakpoints, IconWrapper, RowButton } from '../shared'
|
||||||
|
import state, { useSelector } from 'state'
|
||||||
|
|
||||||
interface Props {
|
function handleIsFilledChange(isFilled: boolean) {
|
||||||
isFilled: boolean
|
state.send('CHANGED_STYLE', { isFilled })
|
||||||
onChange: (isFilled: boolean | string) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IsFilledPicker({
|
export default function IsFilledPicker(): JSX.Element {
|
||||||
isFilled,
|
const isFilled = useSelector((s) => s.values.selectedStyle.isFilled)
|
||||||
onChange,
|
|
||||||
}: Props): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<Checkbox.Root
|
<Checkbox.Root
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
as={RowButton}
|
as={RowButton}
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={breakpoints}
|
||||||
checked={isFilled}
|
checked={isFilled}
|
||||||
onCheckedChange={onChange}
|
onCheckedChange={handleIsFilledChange}
|
||||||
>
|
>
|
||||||
<label htmlFor="fill">Fill</label>
|
<label htmlFor="fill">Fill</label>
|
||||||
<IconWrapper>
|
<IconWrapper>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { IconButton } from 'components/shared'
|
import { breakpoints, IconButton } from 'components/shared'
|
||||||
import Tooltip from 'components/tooltip'
|
import Tooltip from 'components/tooltip'
|
||||||
import { strokes } from 'state/shape-styles'
|
import { strokes } from 'state/shape-styles'
|
||||||
import { Square } from 'react-feather'
|
import { Square } from 'react-feather'
|
||||||
import state, { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import ColorContent from './color-content'
|
import ColorContent from './color-content'
|
||||||
|
|
||||||
export default function QuickColorSelect(): JSX.Element {
|
export default function QuickColorSelect(): JSX.Element {
|
||||||
|
@ -11,17 +11,12 @@ export default function QuickColorSelect(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root dir="ltr">
|
<DropdownMenu.Root dir="ltr">
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
|
||||||
as={IconButton}
|
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
|
||||||
>
|
|
||||||
<Tooltip label="Color">
|
<Tooltip label="Color">
|
||||||
<Square fill={strokes[color]} stroke={strokes[color]} />
|
<Square fill={strokes[color]} stroke={strokes[color]} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<ColorContent
|
<ColorContent />
|
||||||
onChange={(color) => state.send('CHANGED_STYLE', { color })}
|
|
||||||
/>
|
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { IconButton } from 'components/shared'
|
import { breakpoints, IconButton } from 'components/shared'
|
||||||
import Tooltip from 'components/tooltip'
|
import Tooltip from 'components/tooltip'
|
||||||
|
import { memo } from 'react'
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import { DashStyle } from 'types'
|
import { DashStyle } from 'types'
|
||||||
import {
|
import {
|
||||||
|
@ -17,40 +18,35 @@ const dashes = {
|
||||||
[DashStyle.Dotted]: <DashDottedIcon />,
|
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuickdashSelect(): JSX.Element {
|
function changeDashStyle(
|
||||||
|
e: Event & { currentTarget: { value: DashStyle } }
|
||||||
|
): void {
|
||||||
|
state.send('CHANGED_STYLE', { dash: e.currentTarget.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuickdashSelect(): JSX.Element {
|
||||||
const dash = useSelector((s) => s.values.selectedStyle.dash)
|
const dash = useSelector((s) => s.values.selectedStyle.dash)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root dir="ltr">
|
<DropdownMenu.Root dir="ltr">
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
|
||||||
as={IconButton}
|
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
|
||||||
>
|
|
||||||
<Tooltip label="Dash">{dashes[dash]}</Tooltip>
|
<Tooltip label="Dash">{dashes[dash]}</Tooltip>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownContent sideOffset={8} direction="vertical">
|
<DropdownContent sideOffset={8} direction="vertical">
|
||||||
<DashItem isActive={dash === DashStyle.Solid} dash={DashStyle.Solid} />
|
{Object.keys(DashStyle).map((dashStyle: DashStyle) => (
|
||||||
<DashItem
|
<DropdownMenu.DropdownMenuItem
|
||||||
isActive={dash === DashStyle.Dashed}
|
as={Item}
|
||||||
dash={DashStyle.Dashed}
|
key={dashStyle}
|
||||||
/>
|
isActive={dash === dashStyle}
|
||||||
<DashItem
|
onSelect={changeDashStyle}
|
||||||
isActive={dash === DashStyle.Dotted}
|
value={dashStyle}
|
||||||
dash={DashStyle.Dotted}
|
>
|
||||||
/>
|
{dashes[dashStyle]}
|
||||||
|
</DropdownMenu.DropdownMenuItem>
|
||||||
|
))}
|
||||||
</DropdownContent>
|
</DropdownContent>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DashItem({ dash, isActive }: { isActive: boolean; dash: DashStyle }) {
|
export default memo(QuickdashSelect)
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
as={DropdownMenu.DropdownMenuItem}
|
|
||||||
isActive={isActive}
|
|
||||||
onSelect={() => state.send('CHANGED_STYLE', { dash })}
|
|
||||||
>
|
|
||||||
{dashes[dash]}
|
|
||||||
</Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { IconButton } from 'components/shared'
|
import { breakpoints, IconButton } from 'components/shared'
|
||||||
import Tooltip from 'components/tooltip'
|
import Tooltip from 'components/tooltip'
|
||||||
|
import { memo } from 'react'
|
||||||
import { Circle } from 'react-feather'
|
import { Circle } from 'react-feather'
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import { SizeStyle } from 'types'
|
import { SizeStyle } from 'types'
|
||||||
|
@ -12,39 +13,37 @@ const sizes = {
|
||||||
[SizeStyle.Large]: 22,
|
[SizeStyle.Large]: 22,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuickSizeSelect(): JSX.Element {
|
function handleSizeChange(
|
||||||
|
e: Event & { currentTarget: { value: SizeStyle } }
|
||||||
|
): void {
|
||||||
|
state.send('CHANGED_STYLE', { size: e.currentTarget.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuickSizeSelect(): JSX.Element {
|
||||||
const size = useSelector((s) => s.values.selectedStyle.size)
|
const size = useSelector((s) => s.values.selectedStyle.size)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root dir="ltr">
|
<DropdownMenu.Root dir="ltr">
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
|
||||||
as={IconButton}
|
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
|
||||||
>
|
|
||||||
<Tooltip label="Size">
|
<Tooltip label="Size">
|
||||||
<Circle size={sizes[size]} stroke="none" fill="currentColor" />
|
<Circle size={sizes[size]} stroke="none" fill="currentColor" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownContent sideOffset={8} direction="vertical">
|
<DropdownContent sideOffset={8} direction="vertical">
|
||||||
<SizeItem isActive={size === SizeStyle.Small} size={SizeStyle.Small} />
|
{Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => (
|
||||||
<SizeItem
|
<DropdownMenu.DropdownMenuItem
|
||||||
isActive={size === SizeStyle.Medium}
|
key={sizeStyle}
|
||||||
size={SizeStyle.Medium}
|
as={Item}
|
||||||
/>
|
isActive={size === sizeStyle}
|
||||||
<SizeItem isActive={size === SizeStyle.Large} size={SizeStyle.Large} />
|
value={sizeStyle}
|
||||||
|
onSelect={handleSizeChange}
|
||||||
|
>
|
||||||
|
<Circle size={sizes[sizeStyle]} />
|
||||||
|
</DropdownMenu.DropdownMenuItem>
|
||||||
|
))}
|
||||||
</DropdownContent>
|
</DropdownContent>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SizeItem({ size, isActive }: { isActive: boolean; size: SizeStyle }) {
|
export default memo(QuickSizeSelect)
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
as={DropdownMenu.DropdownMenuItem}
|
|
||||||
isActive={isActive}
|
|
||||||
onSelect={() => state.send('CHANGED_STYLE', { size })}
|
|
||||||
>
|
|
||||||
<Circle size={sizes[size]} />
|
|
||||||
</Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
217
components/style-panel/shapes-functions.tsx
Normal file
217
components/style-panel/shapes-functions.tsx
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
import { IconButton, breakpoints } from 'components/shared'
|
||||||
|
import { memo } from 'react'
|
||||||
|
import styled from 'styles'
|
||||||
|
import { MoveType } from 'types'
|
||||||
|
import { Trash2 } from 'react-feather'
|
||||||
|
import state, { useSelector } from 'state'
|
||||||
|
import Tooltip from 'components/tooltip'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
AspectRatioIcon,
|
||||||
|
BoxIcon,
|
||||||
|
CopyIcon,
|
||||||
|
EyeClosedIcon,
|
||||||
|
EyeOpenIcon,
|
||||||
|
LockClosedIcon,
|
||||||
|
LockOpen1Icon,
|
||||||
|
PinBottomIcon,
|
||||||
|
PinTopIcon,
|
||||||
|
RotateCounterClockwiseIcon,
|
||||||
|
} from '@radix-ui/react-icons'
|
||||||
|
import { getPage, getSelectedIds } from 'utils'
|
||||||
|
|
||||||
|
function handleRotateCcw() {
|
||||||
|
state.send('ROTATED_CCW')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDuplicate() {
|
||||||
|
state.send('DUPLICATED')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHide() {
|
||||||
|
state.send('TOGGLED_SHAPE_HIDE')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLock() {
|
||||||
|
state.send('TOGGLED_SHAPE_LOCK')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAspectLock() {
|
||||||
|
state.send('TOGGLED_SHAPE_ASPECT_LOCK')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveToBack() {
|
||||||
|
state.send('MOVED', { type: MoveType.ToBack })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveBackward() {
|
||||||
|
state.send('MOVED', { type: MoveType.Backward })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveForward() {
|
||||||
|
state.send('MOVED', { type: MoveType.Forward })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveToFront() {
|
||||||
|
state.send('MOVED', { type: MoveType.ToFront })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
state.send('DELETED')
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShapesFunctions() {
|
||||||
|
const isAllLocked = useSelector((s) => {
|
||||||
|
const page = getPage(s.data)
|
||||||
|
return s.values.selectedIds.every((id) => page.shapes[id].isLocked)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isAllAspectLocked = useSelector((s) => {
|
||||||
|
const page = getPage(s.data)
|
||||||
|
return s.values.selectedIds.every(
|
||||||
|
(id) => page.shapes[id].isAspectRatioLocked
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isAllHidden = useSelector((s) => {
|
||||||
|
const page = getPage(s.data)
|
||||||
|
return s.values.selectedIds.every((id) => page.shapes[id].isHidden)
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasSelection = useSelector((s) => {
|
||||||
|
return getSelectedIds(s.data).size > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ButtonsRow>
|
||||||
|
<IconButton
|
||||||
|
bp={breakpoints}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleDuplicate}
|
||||||
|
>
|
||||||
|
<Tooltip label="Duplicate">
|
||||||
|
<CopyIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleRotateCcw}
|
||||||
|
>
|
||||||
|
<Tooltip label="Rotate">
|
||||||
|
<RotateCounterClockwiseIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
bp={breakpoints}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleHide}
|
||||||
|
>
|
||||||
|
<Tooltip label="Toogle Hidden">
|
||||||
|
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
bp={breakpoints}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleLock}
|
||||||
|
>
|
||||||
|
<Tooltip label="Toogle Locked">
|
||||||
|
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
bp={breakpoints}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleAspectLock}
|
||||||
|
>
|
||||||
|
<Tooltip label="Toogle Aspect Ratio Lock">
|
||||||
|
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
</ButtonsRow>
|
||||||
|
<ButtonsRow>
|
||||||
|
<IconButton
|
||||||
|
bp={breakpoints}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleMoveToBack}
|
||||||
|
>
|
||||||
|
<Tooltip label="Move to Back">
|
||||||
|
<PinBottomIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
bp={breakpoints}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleMoveBackward}
|
||||||
|
>
|
||||||
|
<Tooltip label="Move Backward">
|
||||||
|
<ArrowDownIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
bp={breakpoints}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleMoveForward}
|
||||||
|
>
|
||||||
|
<Tooltip label="Move Forward">
|
||||||
|
<ArrowUpIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
bp={breakpoints}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleMoveToFront}
|
||||||
|
>
|
||||||
|
<Tooltip label="More to Front">
|
||||||
|
<PinTopIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
bp={breakpoints}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
size="small"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Tooltip label="Delete">
|
||||||
|
<Trash2 size="15" />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
</ButtonsRow>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ShapesFunctions)
|
||||||
|
|
||||||
|
const ButtonsRow = styled('div', {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
outline: 'none',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
padding: 4,
|
||||||
|
})
|
|
@ -1,37 +1,37 @@
|
||||||
import { Group, Item } from '../shared'
|
import { Group, Item } from '../shared'
|
||||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||||
import { Circle } from 'react-feather'
|
import { Circle } from 'react-feather'
|
||||||
import state from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import { SizeStyle } from 'types'
|
import { SizeStyle } from 'types'
|
||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
[SizeStyle.Small]: 6,
|
||||||
|
[SizeStyle.Medium]: 12,
|
||||||
|
[SizeStyle.Large]: 22,
|
||||||
|
}
|
||||||
|
|
||||||
function handleChange(size: string) {
|
function handleChange(size: string) {
|
||||||
state.send('CHANGED_STYLE', { size })
|
state.send('CHANGED_STYLE', { size })
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SizePicker({ size }: { size: SizeStyle }): JSX.Element {
|
function SizePicker(): JSX.Element {
|
||||||
|
const size = useSelector((s) => s.values.selectedStyle.size)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="width" onValueChange={handleChange}>
|
<Group name="width" onValueChange={handleChange}>
|
||||||
<Item
|
{Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => (
|
||||||
as={RadioGroup.Item}
|
<RadioGroup.Item
|
||||||
value={SizeStyle.Small}
|
key={sizeStyle}
|
||||||
isActive={size === SizeStyle.Small}
|
as={Item}
|
||||||
>
|
isActive={size === sizeStyle}
|
||||||
<Circle size={6} />
|
value={sizeStyle}
|
||||||
</Item>
|
>
|
||||||
<Item
|
<Circle size={sizes[sizeStyle]} />
|
||||||
as={RadioGroup.Item}
|
</RadioGroup.Item>
|
||||||
value={SizeStyle.Medium}
|
))}
|
||||||
isActive={size === SizeStyle.Medium}
|
|
||||||
>
|
|
||||||
<Circle size={12} />
|
|
||||||
</Item>
|
|
||||||
<Item
|
|
||||||
as={RadioGroup.Item}
|
|
||||||
value={SizeStyle.Large}
|
|
||||||
isActive={size === SizeStyle.Large}
|
|
||||||
>
|
|
||||||
<Circle size={22} />
|
|
||||||
</Item>
|
|
||||||
</Group>
|
</Group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(SizePicker)
|
||||||
|
|
|
@ -3,31 +3,10 @@ import state, { useSelector } from 'state'
|
||||||
import * as Panel from 'components/panel'
|
import * as Panel from 'components/panel'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { IconButton } from 'components/shared'
|
import { IconButton } from 'components/shared'
|
||||||
import { ChevronDown, Trash2, X } from 'react-feather'
|
import { ChevronDown, X } from 'react-feather'
|
||||||
import {
|
import ShapesFunctions from './shapes-functions'
|
||||||
deepCompare,
|
|
||||||
deepCompareArrays,
|
|
||||||
getPage,
|
|
||||||
getSelectedIds,
|
|
||||||
setToArray,
|
|
||||||
} from 'utils'
|
|
||||||
import AlignDistribute from './align-distribute'
|
import AlignDistribute from './align-distribute'
|
||||||
import { MoveType } from 'types'
|
|
||||||
import SizePicker from './size-picker'
|
import SizePicker from './size-picker'
|
||||||
import {
|
|
||||||
ArrowDownIcon,
|
|
||||||
ArrowUpIcon,
|
|
||||||
AspectRatioIcon,
|
|
||||||
BoxIcon,
|
|
||||||
CopyIcon,
|
|
||||||
EyeClosedIcon,
|
|
||||||
EyeOpenIcon,
|
|
||||||
LockClosedIcon,
|
|
||||||
LockOpen1Icon,
|
|
||||||
PinBottomIcon,
|
|
||||||
PinTopIcon,
|
|
||||||
RotateCounterClockwiseIcon,
|
|
||||||
} from '@radix-ui/react-icons'
|
|
||||||
import DashPicker from './dash-picker'
|
import DashPicker from './dash-picker'
|
||||||
import QuickColorSelect from './quick-color-select'
|
import QuickColorSelect from './quick-color-select'
|
||||||
import ColorPicker from './color-picker'
|
import ColorPicker from './color-picker'
|
||||||
|
@ -37,23 +16,12 @@ import QuickdashSelect from './quick-dash-select'
|
||||||
import Tooltip from 'components/tooltip'
|
import Tooltip from 'components/tooltip'
|
||||||
|
|
||||||
const breakpoints = { '@initial': 'mobile', '@sm': 'small' } as any
|
const breakpoints = { '@initial': 'mobile', '@sm': 'small' } as any
|
||||||
|
|
||||||
const handleStylePanelOpen = () => state.send('TOGGLED_STYLE_PANEL_OPEN')
|
const handleStylePanelOpen = () => state.send('TOGGLED_STYLE_PANEL_OPEN')
|
||||||
const handleColorChange = (color) => state.send('CHANGED_STYLE', { color })
|
|
||||||
const handleRotateCcw = () => () => state.send('ROTATED_CCW')
|
|
||||||
const handleIsFilledChange = (dash) => state.send('CHANGED_STYLE', { dash })
|
|
||||||
const handleDuplicate = () => state.send('DUPLICATED')
|
|
||||||
const handleHide = () => state.send('TOGGLED_SHAPE_HIDE')
|
|
||||||
const handleLock = () => state.send('TOGGLED_SHAPE_LOCK')
|
|
||||||
const handleAspectLock = () => state.send('TOGGLED_SHAPE_ASPECT_LOCK')
|
|
||||||
const handleMoveToBack = () => state.send('MOVED', { type: MoveType.ToBack })
|
|
||||||
const handleMoveBackward = () =>
|
|
||||||
state.send('MOVED', { type: MoveType.Backward })
|
|
||||||
const handleMoveForward = () => state.send('MOVED', { type: MoveType.Forward })
|
|
||||||
const handleMoveToFront = () => state.send('MOVED', { type: MoveType.ToFront })
|
|
||||||
const handleDelete = () => state.send('DELETED')
|
|
||||||
|
|
||||||
export default function StylePanel(): JSX.Element {
|
export default function StylePanel(): JSX.Element {
|
||||||
const rContainer = useRef<HTMLDivElement>(null)
|
const rContainer = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
|
const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -86,29 +54,7 @@ export default function StylePanel(): JSX.Element {
|
||||||
// track of this data manually within our state.
|
// track of this data manually within our state.
|
||||||
|
|
||||||
function SelectedShapeStyles(): JSX.Element {
|
function SelectedShapeStyles(): JSX.Element {
|
||||||
const selectedIds = useSelector(
|
const selectedShapesCount = useSelector((s) => s.values.selectedIds.length)
|
||||||
(s) => setToArray(getSelectedIds(s.data)),
|
|
||||||
deepCompareArrays
|
|
||||||
)
|
|
||||||
|
|
||||||
const isAllLocked = useSelector((s) => {
|
|
||||||
const page = getPage(s.data)
|
|
||||||
return selectedIds.every((id) => page.shapes[id].isLocked)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isAllAspectLocked = useSelector((s) => {
|
|
||||||
const page = getPage(s.data)
|
|
||||||
return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isAllHidden = useSelector((s) => {
|
|
||||||
const page = getPage(s.data)
|
|
||||||
return selectedIds.every((id) => page.shapes[id].isHidden)
|
|
||||||
})
|
|
||||||
|
|
||||||
const commonStyle = useSelector((s) => s.values.selectedStyle, deepCompare)
|
|
||||||
|
|
||||||
const hasSelection = selectedIds.length > 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Layout>
|
<Panel.Layout>
|
||||||
|
@ -123,133 +69,20 @@ function SelectedShapeStyles(): JSX.Element {
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Panel.Header>
|
</Panel.Header>
|
||||||
<Content>
|
<Content>
|
||||||
<ColorPicker color={commonStyle.color} onChange={handleColorChange} />
|
<ColorPicker />
|
||||||
<IsFilledPicker
|
<IsFilledPicker />
|
||||||
isFilled={commonStyle.isFilled}
|
|
||||||
onChange={handleIsFilledChange}
|
|
||||||
/>
|
|
||||||
<Row>
|
<Row>
|
||||||
<label htmlFor="size">Size</label>
|
<label htmlFor="size">Size</label>
|
||||||
<SizePicker size={commonStyle.size} />
|
<SizePicker />
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<label htmlFor="dash">Dash</label>
|
<label htmlFor="dash">Dash</label>
|
||||||
<DashPicker dash={commonStyle.dash} />
|
<DashPicker />
|
||||||
</Row>
|
</Row>
|
||||||
<ButtonsRow>
|
<ShapesFunctions />
|
||||||
<IconButton
|
|
||||||
bp={breakpoints}
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleDuplicate}
|
|
||||||
>
|
|
||||||
<Tooltip label="Duplicate">
|
|
||||||
<CopyIcon />
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleRotateCcw}
|
|
||||||
>
|
|
||||||
<Tooltip label="Rotate">
|
|
||||||
<RotateCounterClockwiseIcon />
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
bp={breakpoints}
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleHide}
|
|
||||||
>
|
|
||||||
<Tooltip label="Toogle Hidden">
|
|
||||||
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
bp={breakpoints}
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleLock}
|
|
||||||
>
|
|
||||||
<Tooltip label="Toogle Locked">
|
|
||||||
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
bp={breakpoints}
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleAspectLock}
|
|
||||||
>
|
|
||||||
<Tooltip label="Toogle Aspect Ratio Lock">
|
|
||||||
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
</ButtonsRow>
|
|
||||||
<ButtonsRow>
|
|
||||||
<IconButton
|
|
||||||
bp={breakpoints}
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleMoveToBack}
|
|
||||||
>
|
|
||||||
<Tooltip label="Move to Back">
|
|
||||||
<PinBottomIcon />
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
bp={breakpoints}
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleMoveBackward}
|
|
||||||
>
|
|
||||||
<Tooltip label="Move Backward">
|
|
||||||
<ArrowDownIcon />
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
bp={breakpoints}
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleMoveForward}
|
|
||||||
>
|
|
||||||
<Tooltip label="Move Forward">
|
|
||||||
<ArrowUpIcon />
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
bp={breakpoints}
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleMoveToFront}
|
|
||||||
>
|
|
||||||
<Tooltip label="More to Front">
|
|
||||||
<PinTopIcon />
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
bp={breakpoints}
|
|
||||||
disabled={!hasSelection}
|
|
||||||
size="small"
|
|
||||||
onClick={handleDelete}
|
|
||||||
>
|
|
||||||
<Tooltip label="Delete">
|
|
||||||
<Trash2 size="15" />
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
</ButtonsRow>
|
|
||||||
<AlignDistribute
|
<AlignDistribute
|
||||||
hasTwoOrMore={selectedIds.length > 1}
|
hasTwoOrMore={selectedShapesCount > 1}
|
||||||
hasThreeOrMore={selectedIds.length > 2}
|
hasThreeOrMore={selectedShapesCount > 2}
|
||||||
/>
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
</Panel.Layout>
|
</Panel.Layout>
|
||||||
|
@ -306,16 +139,3 @@ const Row = styled('div', {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const ButtonsRow = styled('div', {
|
|
||||||
position: 'relative',
|
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
|
||||||
background: 'none',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
outline: 'none',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
padding: 4,
|
|
||||||
})
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function usePageShapes(): string[] {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Get the shapes that fit into the current window
|
// Get the shapes that fit into the current window
|
||||||
return useSelector((s) => {
|
const visiblePageShapeIds = useSelector((s) => {
|
||||||
const pageState = getPageState(s.data)
|
const pageState = getPageState(s.data)
|
||||||
|
|
||||||
if (!viewportCache.has(pageState)) {
|
if (!viewportCache.has(pageState)) {
|
||||||
|
@ -46,4 +46,6 @@ export default function usePageShapes(): string[] {
|
||||||
})
|
})
|
||||||
.map((shape) => shape.id)
|
.map((shape) => shape.id)
|
||||||
}, deepCompareArrays)
|
}, deepCompareArrays)
|
||||||
|
|
||||||
|
return visiblePageShapeIds
|
||||||
}
|
}
|
||||||
|
|
|
@ -1870,9 +1870,17 @@ const state = createState({
|
||||||
data.boundsRotation = 0
|
data.boundsRotation = 0
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
asyncs: {
|
||||||
|
async getUpdatedShapes(data) {
|
||||||
|
return updateFromCode(
|
||||||
|
data,
|
||||||
|
data.document.code[data.currentCodeFileId].code
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
selectedIds(data) {
|
selectedIds(data) {
|
||||||
return new Set(getSelectedIds(data))
|
return setToArray(getSelectedIds(data))
|
||||||
},
|
},
|
||||||
selectedBounds(data) {
|
selectedBounds(data) {
|
||||||
return getSelectionBounds(data)
|
return getSelectionBounds(data)
|
||||||
|
@ -1915,14 +1923,6 @@ const state = createState({
|
||||||
return commonStyle
|
return commonStyle
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
asyncs: {
|
|
||||||
async getUpdatedShapes(data) {
|
|
||||||
return updateFromCode(
|
|
||||||
data,
|
|
||||||
data.document.code[data.currentCodeFileId].code
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default state
|
export default state
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue