kopia lustrzana https://github.com/Tldraw/Tldraw
perf improvements around selected / hovered shapes
rodzic
2cfeea0449
commit
8ff8b87a9e
|
@ -2,11 +2,9 @@ import * as React from 'react'
|
|||
import { Edge, Corner } from 'types'
|
||||
import { useSelector } from 'state'
|
||||
import {
|
||||
deepCompareArrays,
|
||||
getBoundsCenter,
|
||||
getCurrentCamera,
|
||||
getPage,
|
||||
getSelectedIds,
|
||||
getSelectedShapes,
|
||||
isMobile,
|
||||
} from 'utils'
|
||||
|
@ -24,25 +22,22 @@ export default function Bounds(): JSX.Element {
|
|||
|
||||
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||
|
||||
const selectedIds = useSelector(
|
||||
(s) => Array.from(s.values.selectedIds.values()),
|
||||
deepCompareArrays
|
||||
)
|
||||
|
||||
const rotation = useSelector(({ data }) =>
|
||||
getSelectedIds(data).size === 1 ? getSelectedShapes(data)[0].rotation : 0
|
||||
const rotation = useSelector((s) =>
|
||||
s.values.selectedIds.length === 1
|
||||
? getSelectedShapes(s.data)[0].rotation
|
||||
: 0
|
||||
)
|
||||
|
||||
const isAllLocked = useSelector((s) => {
|
||||
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 page = getPage(s.data)
|
||||
return (
|
||||
selectedIds.length === 1 &&
|
||||
page.shapes[selectedIds[0]]?.handles !== undefined
|
||||
s.values.selectedIds.length === 1 &&
|
||||
page.shapes[s.values.selectedIds[0]]?.handles !== undefined
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useRef } from 'react'
|
|||
import state, { useSelector } from 'state'
|
||||
import inputs from 'state/inputs'
|
||||
import styled from 'styles'
|
||||
import { deepCompareArrays, getPage } from 'utils'
|
||||
import { getPage } from 'utils'
|
||||
|
||||
function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
|
@ -31,28 +31,30 @@ export default function BoundsBg(): JSX.Element {
|
|||
|
||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||
|
||||
const selectedIds = useSelector(
|
||||
(s) => Array.from(s.values.selectedIds.values()),
|
||||
deepCompareArrays
|
||||
)
|
||||
|
||||
const rotation = useSelector((s) => {
|
||||
const selectedIds = s.values.selectedIds
|
||||
|
||||
if (selectedIds.length === 1) {
|
||||
const { shapes } = getPage(s.data)
|
||||
const selected = Array.from(s.values.selectedIds.values())[0]
|
||||
return shapes[selected]?.rotation
|
||||
const selected = selectedIds[0]
|
||||
const page = getPage(s.data)
|
||||
|
||||
return page.shapes[selected]?.rotation
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
const isAllHandles = useSelector((s) => {
|
||||
const page = getPage(s.data)
|
||||
const selectedIds = Array.from(s.values.selectedIds.values())
|
||||
return (
|
||||
selectedIds.length === 1 &&
|
||||
page.shapes[selectedIds[0]]?.handles !== undefined
|
||||
)
|
||||
const selectedIds = s.values.selectedIds
|
||||
|
||||
if (selectedIds.length === 1) {
|
||||
const page = getPage(s.data)
|
||||
const selected = selectedIds[0]
|
||||
|
||||
return (
|
||||
selectedIds.length === 1 && page.shapes[selected]?.handles !== undefined
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (isAllHandles) return null
|
||||
|
|
|
@ -3,18 +3,14 @@ import { getShapeUtils } from 'state/shape-utils'
|
|||
import { useRef } from 'react'
|
||||
import { useSelector } from 'state'
|
||||
import styled from 'styles'
|
||||
import { deepCompareArrays, getPage } from 'utils'
|
||||
import { getPage } from 'utils'
|
||||
import vec from 'utils/vec'
|
||||
|
||||
export default function Handles(): JSX.Element {
|
||||
const selectedIds = useSelector(
|
||||
(s) => Array.from(s.values.selectedIds.values()),
|
||||
deepCompareArrays
|
||||
)
|
||||
|
||||
const shape = useSelector(
|
||||
({ data }) =>
|
||||
selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]]
|
||||
(s) =>
|
||||
s.values.selectedIds.length === 1 &&
|
||||
getPage(s.data).shapes[s.values.selectedIds[0]]
|
||||
)
|
||||
|
||||
const isSelecting = useSelector((s) =>
|
||||
|
|
|
@ -13,6 +13,10 @@ import Handles from './bounds/handles'
|
|||
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||
import ContextMenu from './context-menu/context-menu'
|
||||
|
||||
function resetError() {
|
||||
null
|
||||
}
|
||||
|
||||
export default function Canvas(): JSX.Element {
|
||||
const rCanvas = useRef<SVGSVGElement>(null)
|
||||
const rGroup = useRef<SVGGElement>(null)
|
||||
|
@ -28,12 +32,7 @@ export default function Canvas(): JSX.Element {
|
|||
return (
|
||||
<ContextMenu>
|
||||
<MainSVG ref={rCanvas} {...events}>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorFallback}
|
||||
onReset={() => {
|
||||
// reset the state of your app so the error doesn't happen again
|
||||
}}
|
||||
>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError}>
|
||||
<Defs />
|
||||
{isReady && (
|
||||
<g ref={rGroup} id="shapes">
|
||||
|
|
|
@ -5,14 +5,7 @@ import {
|
|||
IconButton as _IconButton,
|
||||
RowButton,
|
||||
} from 'components/shared'
|
||||
import {
|
||||
commandKey,
|
||||
deepCompareArrays,
|
||||
getSelectedIds,
|
||||
getShape,
|
||||
isMobile,
|
||||
setToArray,
|
||||
} from 'utils'
|
||||
import { commandKey, deepCompareArrays, getShape, isMobile } from 'utils'
|
||||
import state, { useSelector } from 'state'
|
||||
import {
|
||||
AlignType,
|
||||
|
@ -82,7 +75,7 @@ export default function ContextMenu({
|
|||
children: React.ReactNode
|
||||
}): JSX.Element {
|
||||
const selectedShapeIds = useSelector(
|
||||
(s) => setToArray(getSelectedIds(s.data)),
|
||||
(s) => s.values.selectedIds,
|
||||
deepCompareArrays
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { getShapeStyle } from 'state/shape-styles'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import React, { memo } from 'react'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'state'
|
||||
import { getCurrentCamera } from 'utils'
|
||||
import { DotCircle, Handle } from './misc'
|
||||
|
@ -12,28 +12,32 @@ export default function Defs(): JSX.Element {
|
|||
|
||||
return (
|
||||
<defs>
|
||||
{shapeIdsToRender.map((id) => (
|
||||
<Def key={id} id={id} />
|
||||
))}
|
||||
<DotCircle id="dot" r={4} />
|
||||
<Handle id="handle" r={4} />
|
||||
<ExpandDef />
|
||||
{shapeIdsToRender.map((id) => (
|
||||
<Def key={id} id={id} />
|
||||
))}
|
||||
</defs>
|
||||
)
|
||||
}
|
||||
|
||||
const Def = memo(function Def({ id }: { id: string }) {
|
||||
function Def({ id }: { id: string }) {
|
||||
const shape = useShapeDef(id)
|
||||
|
||||
if (!shape) return null
|
||||
|
||||
const style = getShapeStyle(shape.style)
|
||||
|
||||
return React.cloneElement(
|
||||
getShapeUtils(shape).render(shape, { isEditing: false }),
|
||||
{ id, ...style }
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(
|
||||
getShapeUtils(shape).render(shape, { isEditing: false }),
|
||||
{ id, ...style, strokeWidth: undefined }
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function ExpandDef() {
|
||||
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
|
||||
|
|
|
@ -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 Shape from './shape'
|
||||
import HoveredShape from './hovered-shape'
|
||||
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.
|
||||
*/
|
||||
|
||||
const noOffset = [0, 0]
|
||||
|
||||
export default function Page(): JSX.Element {
|
||||
const currentPageShapeIds = usePageShapes()
|
||||
|
||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||
|
||||
const visiblePageShapeIds = usePageShapes()
|
||||
|
||||
const hoveredShapeId = useSelector((s) => {
|
||||
return visiblePageShapeIds.find((id) => id === s.data.hoveredId)
|
||||
})
|
||||
|
||||
return (
|
||||
<g pointerEvents={isSelecting ? 'all' : 'none'}>
|
||||
{currentPageShapeIds.map((shapeId) => (
|
||||
<Shape
|
||||
key={shapeId}
|
||||
id={shapeId}
|
||||
isSelecting={isSelecting}
|
||||
parentPoint={noOffset}
|
||||
/>
|
||||
{isSelecting && hoveredShapeId && (
|
||||
<HoveredShape key={hoveredShapeId} id={hoveredShapeId} />
|
||||
)}
|
||||
{visiblePageShapeIds.map((id) => (
|
||||
<Shape key={id} id={id} isSelecting={isSelecting} />
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import styled from 'styles'
|
||||
import { useSelector } from 'state'
|
||||
import { deepCompareArrays, getPage, getSelectedIds, setToArray } from 'utils'
|
||||
import { deepCompareArrays, getPage } from 'utils'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import { memo } from 'react'
|
||||
|
||||
export default function Selected(): JSX.Element {
|
||||
const currentSelectedShapeIds = useSelector(
|
||||
({ data }) => setToArray(getSelectedIds(data)),
|
||||
(s) => s.values.selectedIds,
|
||||
deepCompareArrays
|
||||
)
|
||||
|
||||
|
|
|
@ -2,31 +2,150 @@ import React, { useRef, memo, useEffect } from 'react'
|
|||
import { useSelector } from 'state'
|
||||
import styled from 'styles'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import { getPage, getSelectedIds, isMobile } from 'utils'
|
||||
import { deepCompareArrays, getPage, getShape } from 'utils'
|
||||
import useShapeEvents from 'hooks/useShapeEvents'
|
||||
import { Shape as _Shape } from 'types'
|
||||
import vec from 'utils/vec'
|
||||
import { getShapeStyle } from 'state/shape-styles'
|
||||
|
||||
const isMobileDevice = isMobile()
|
||||
import useShapeDef from 'hooks/useShape'
|
||||
|
||||
interface ShapeProps {
|
||||
id: string
|
||||
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 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 isEditing = useSelector((s) => s.data.editingId === id)
|
||||
|
||||
const isSelected = useSelector((s) => getSelectedIds(s.data).has(id))
|
||||
|
||||
const shape = useSelector((s) => getPage(s.data).shapes[id])
|
||||
|
||||
const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup)
|
||||
const shapeUtils = getShapeUtils(shape)
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
|
@ -38,85 +157,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps): JSX.Element {
|
|||
}
|
||||
}, [isEditing])
|
||||
|
||||
// This is a problem with deleted shapes. The hooks in this component
|
||||
// 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} />
|
||||
return shapeUtils.render(shape, { isEditing, ref: rFocusable })
|
||||
})
|
||||
|
||||
const StyledShape = styled('path', {
|
||||
|
@ -125,12 +166,10 @@ const StyledShape = styled('path', {
|
|||
pointerEvents: 'none',
|
||||
})
|
||||
|
||||
const HoverIndicator = styled('path', {
|
||||
stroke: '$selected',
|
||||
const EventSoak = styled('use', {
|
||||
opacity: 0,
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
fill: 'transparent',
|
||||
filter: 'url(#expand)',
|
||||
variants: {
|
||||
variant: {
|
||||
ghost: {
|
||||
|
@ -150,81 +189,6 @@ const HoverIndicator = styled('path', {
|
|||
|
||||
const StyledGroup = styled('g', {
|
||||
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)
|
||||
|
|
|
@ -3,40 +3,44 @@ import styled from 'styles'
|
|||
import React, { useRef } from 'react'
|
||||
import state, { useSelector } from 'state'
|
||||
import { X, Code } from 'react-feather'
|
||||
import { IconButton } from 'components/shared'
|
||||
import { breakpoints, IconButton } from 'components/shared'
|
||||
import * as Panel from '../panel'
|
||||
import Control from './control'
|
||||
import { deepCompareArrays } from 'utils'
|
||||
|
||||
function openCodePanel() {
|
||||
state.send('CLOSED_CODE_PANEL')
|
||||
}
|
||||
|
||||
function closeCodePanel() {
|
||||
state.send('OPENED_CODE_PANEL')
|
||||
}
|
||||
|
||||
const stopKeyboardPropagation = (e: KeyboardEvent | React.KeyboardEvent) =>
|
||||
e.stopPropagation()
|
||||
|
||||
export default function ControlPanel(): JSX.Element {
|
||||
const rContainer = useRef<HTMLDivElement>(null)
|
||||
const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0)
|
||||
const codeControls = useSelector(
|
||||
(state) => Object.keys(state.data.codeControls),
|
||||
deepCompareArrays
|
||||
)
|
||||
const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0)
|
||||
|
||||
return (
|
||||
<Panel.Root
|
||||
ref={rContainer}
|
||||
dir="ltr"
|
||||
data-bp-desktop
|
||||
ref={rContainer}
|
||||
isOpen={isOpen}
|
||||
variant="controls"
|
||||
isOpen={isOpen}
|
||||
onKeyDown={stopKeyboardPropagation}
|
||||
onKeyUp={stopKeyboardPropagation}
|
||||
>
|
||||
{isOpen ? (
|
||||
<Panel.Layout>
|
||||
<Panel.Header>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
size="small"
|
||||
onClick={() => state.send('CLOSED_CODE_PANEL')}
|
||||
>
|
||||
<IconButton bp={breakpoints} size="small" onClick={closeCodePanel}>
|
||||
<X />
|
||||
</IconButton>
|
||||
<h3>Controls</h3>
|
||||
|
@ -48,11 +52,7 @@ export default function ControlPanel(): JSX.Element {
|
|||
</ControlsList>
|
||||
</Panel.Layout>
|
||||
) : (
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
size="small"
|
||||
onClick={() => state.send('OPENED_CODE_PANEL')}
|
||||
>
|
||||
<IconButton bp={breakpoints} size="small" onClick={openCodePanel}>
|
||||
<Code />
|
||||
</IconButton>
|
||||
)}
|
||||
|
|
|
@ -2,7 +2,7 @@ import styled from 'styles'
|
|||
import * as ContextMenu from '@radix-ui/react-context-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 * as Panel from '../panel'
|
||||
import state, { useSelector } from 'state'
|
||||
|
@ -40,8 +40,8 @@ export default function PagePanel(): JSX.Element {
|
|||
<PanelRoot dir="ltr">
|
||||
<DropdownMenu.Trigger
|
||||
as={RowButton}
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
css={{ paddingRight: 12 }}
|
||||
bp={breakpoints}
|
||||
variant="pageButton"
|
||||
>
|
||||
<span>{documentPages[currentPageId].name}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
|
@ -58,11 +58,7 @@ export default function PagePanel(): JSX.Element {
|
|||
{sorted.map(({ id, name }) => (
|
||||
<ContextMenu.Root dir="ltr" key={id}>
|
||||
<ContextMenu.Trigger>
|
||||
<StyledRadioItem
|
||||
key={id}
|
||||
value={id}
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
>
|
||||
<StyledRadioItem key={id} value={id} bp={breakpoints}>
|
||||
<span>{name}</span>
|
||||
<DropdownMenu.ItemIndicator as={IconWrapper} size="small">
|
||||
<CheckIcon />
|
||||
|
@ -91,7 +87,7 @@ export default function PagePanel(): JSX.Element {
|
|||
</DropdownMenu.RadioGroup>
|
||||
<DropdownMenu.Separator />
|
||||
<RowButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
onClick={() => {
|
||||
setIsOpen(false)
|
||||
state.send('CREATED_PAGE')
|
||||
|
|
|
@ -3,6 +3,8 @@ import * as RadioGroup from '@radix-ui/react-radio-group'
|
|||
import * as Panel from './panel'
|
||||
import styled from 'styles'
|
||||
|
||||
export const breakpoints: any = { '@initial': 'mobile', '@sm': 'small' }
|
||||
|
||||
export const IconButton = styled('button', {
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
|
@ -124,6 +126,11 @@ export const RowButton = styled('button', {
|
|||
width: 'auto',
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
pageButton: {
|
||||
paddingRight: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ import {
|
|||
StretchHorizontallyIcon,
|
||||
StretchVerticallyIcon,
|
||||
} 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 styled from 'styles'
|
||||
import { AlignType, DistributeType, StretchType } from 'types'
|
||||
|
@ -55,7 +56,7 @@ function distributeHorizontally() {
|
|||
state.send('DISTRIBUTED', { type: DistributeType.Horizontal })
|
||||
}
|
||||
|
||||
export default function AlignDistribute({
|
||||
function AlignDistribute({
|
||||
hasTwoOrMore,
|
||||
hasThreeOrMore,
|
||||
}: {
|
||||
|
@ -65,7 +66,7 @@ export default function AlignDistribute({
|
|||
return (
|
||||
<Container>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasTwoOrMore}
|
||||
onClick={alignLeft}
|
||||
|
@ -73,7 +74,7 @@ export default function AlignDistribute({
|
|||
<AlignLeftIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasTwoOrMore}
|
||||
onClick={alignCenterHorizontal}
|
||||
|
@ -81,7 +82,7 @@ export default function AlignDistribute({
|
|||
<AlignCenterHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasTwoOrMore}
|
||||
onClick={alignRight}
|
||||
|
@ -89,7 +90,7 @@ export default function AlignDistribute({
|
|||
<AlignRightIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasTwoOrMore}
|
||||
onClick={stretchHorizontally}
|
||||
|
@ -97,7 +98,7 @@ export default function AlignDistribute({
|
|||
<StretchHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasThreeOrMore}
|
||||
onClick={distributeHorizontally}
|
||||
|
@ -105,7 +106,7 @@ export default function AlignDistribute({
|
|||
<SpaceEvenlyHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasTwoOrMore}
|
||||
onClick={alignTop}
|
||||
|
@ -113,7 +114,7 @@ export default function AlignDistribute({
|
|||
<AlignTopIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasTwoOrMore}
|
||||
onClick={alignCenterVertical}
|
||||
|
@ -121,7 +122,7 @@ export default function AlignDistribute({
|
|||
<AlignCenterVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasTwoOrMore}
|
||||
onClick={alignBottom}
|
||||
|
@ -129,7 +130,7 @@ export default function AlignDistribute({
|
|||
<AlignBottomIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasTwoOrMore}
|
||||
onClick={stretchVertically}
|
||||
|
@ -137,7 +138,7 @@ export default function AlignDistribute({
|
|||
<StretchVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
disabled={!hasThreeOrMore}
|
||||
onClick={distributeVertically}
|
||||
|
@ -148,6 +149,8 @@ export default function AlignDistribute({
|
|||
)
|
||||
}
|
||||
|
||||
export default memo(AlignDistribute)
|
||||
|
||||
const Container = styled('div', {
|
||||
display: 'grid',
|
||||
padding: 4,
|
||||
|
|
|
@ -4,12 +4,16 @@ import { ColorStyle } from 'types'
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { Square } from 'react-feather'
|
||||
import { DropdownContent } from '../shared'
|
||||
import { memo } from 'react'
|
||||
import state from 'state'
|
||||
|
||||
export default function ColorContent({
|
||||
onChange,
|
||||
}: {
|
||||
onChange: (color: ColorStyle) => void
|
||||
}): JSX.Element {
|
||||
function handleColorChange(
|
||||
e: Event & { currentTarget: { value: ColorStyle } }
|
||||
): void {
|
||||
state.send('CHANGED_STYLE', { color: e.currentTarget.value })
|
||||
}
|
||||
|
||||
function ColorContent(): JSX.Element {
|
||||
return (
|
||||
<DropdownContent sideOffset={8} side="bottom">
|
||||
{Object.keys(strokes).map((color: ColorStyle) => (
|
||||
|
@ -17,7 +21,8 @@ export default function ColorContent({
|
|||
as={IconButton}
|
||||
key={color}
|
||||
title={color}
|
||||
onSelect={() => onChange(color)}
|
||||
value={color}
|
||||
onSelect={handleColorChange}
|
||||
>
|
||||
<Square fill={strokes[color]} stroke="none" size="22" />
|
||||
</DropdownMenu.DropdownMenuItem>
|
||||
|
@ -25,3 +30,5 @@ export default function ColorContent({
|
|||
</DropdownContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ColorContent)
|
||||
|
|
|
@ -1,28 +1,25 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { strokes } from 'state/shape-styles'
|
||||
import { ColorStyle } from 'types'
|
||||
import { RowButton, IconWrapper } from '../shared'
|
||||
import { RowButton, IconWrapper, breakpoints } from '../shared'
|
||||
import { Square } from 'react-feather'
|
||||
import ColorContent from './color-content'
|
||||
import { memo } from 'react'
|
||||
import { useSelector } from 'state'
|
||||
|
||||
interface Props {
|
||||
color: ColorStyle
|
||||
onChange: (color: ColorStyle) => void
|
||||
}
|
||||
function ColorPicker(): JSX.Element {
|
||||
const color = useSelector((s) => s.values.selectedStyle.color)
|
||||
|
||||
export default function ColorPicker({ color, onChange }: Props): JSX.Element {
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DropdownMenu.Trigger
|
||||
as={RowButton}
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
>
|
||||
<DropdownMenu.Trigger as={RowButton} bp={breakpoints}>
|
||||
<label htmlFor="color">Color</label>
|
||||
<IconWrapper>
|
||||
<Square fill={strokes[color]} />
|
||||
</IconWrapper>
|
||||
</DropdownMenu.Trigger>
|
||||
<ColorContent onChange={onChange} />
|
||||
<ColorContent />
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ColorPicker)
|
||||
|
|
|
@ -7,40 +7,36 @@ import {
|
|||
} from '../shared'
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import { DashStyle } from 'types'
|
||||
import state from 'state'
|
||||
import state, { useSelector } from 'state'
|
||||
import { memo } from 'react'
|
||||
|
||||
function handleChange(dash: string) {
|
||||
state.send('CHANGED_STYLE', { dash })
|
||||
}
|
||||
|
||||
interface Props {
|
||||
dash: DashStyle
|
||||
const dashes = {
|
||||
[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 (
|
||||
<Group name="Dash" onValueChange={handleChange}>
|
||||
<Item
|
||||
as={RadioGroup.RadioGroupItem}
|
||||
value={DashStyle.Solid}
|
||||
isActive={dash === DashStyle.Solid}
|
||||
>
|
||||
<DashSolidIcon />
|
||||
</Item>
|
||||
<Item
|
||||
as={RadioGroup.RadioGroupItem}
|
||||
value={DashStyle.Dashed}
|
||||
isActive={dash === DashStyle.Dashed}
|
||||
>
|
||||
<DashDashedIcon />
|
||||
</Item>
|
||||
<Item
|
||||
as={RadioGroup.RadioGroupItem}
|
||||
value={DashStyle.Dotted}
|
||||
isActive={dash === DashStyle.Dotted}
|
||||
>
|
||||
<DashDottedIcon />
|
||||
</Item>
|
||||
{Object.keys(DashStyle).map((dashStyle: DashStyle) => (
|
||||
<RadioGroup.RadioGroupItem
|
||||
as={Item}
|
||||
key={dashStyle}
|
||||
isActive={dash === dashStyle}
|
||||
value={dashStyle}
|
||||
>
|
||||
{dashes[dashStyle]}
|
||||
</RadioGroup.RadioGroupItem>
|
||||
))}
|
||||
</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 { strokes } from 'state/shape-styles'
|
||||
import { Square } from 'react-feather'
|
||||
import { IconWrapper, RowButton } from '../shared'
|
||||
import { breakpoints, IconWrapper, RowButton } from '../shared'
|
||||
import state, { useSelector } from 'state'
|
||||
|
||||
interface Props {
|
||||
isFilled: boolean
|
||||
onChange: (isFilled: boolean | string) => void
|
||||
function handleIsFilledChange(isFilled: boolean) {
|
||||
state.send('CHANGED_STYLE', { isFilled })
|
||||
}
|
||||
|
||||
export default function IsFilledPicker({
|
||||
isFilled,
|
||||
onChange,
|
||||
}: Props): JSX.Element {
|
||||
export default function IsFilledPicker(): JSX.Element {
|
||||
const isFilled = useSelector((s) => s.values.selectedStyle.isFilled)
|
||||
|
||||
return (
|
||||
<Checkbox.Root
|
||||
dir="ltr"
|
||||
as={RowButton}
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
bp={breakpoints}
|
||||
checked={isFilled}
|
||||
onCheckedChange={onChange}
|
||||
onCheckedChange={handleIsFilledChange}
|
||||
>
|
||||
<label htmlFor="fill">Fill</label>
|
||||
<IconWrapper>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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 { strokes } from 'state/shape-styles'
|
||||
import { Square } from 'react-feather'
|
||||
import state, { useSelector } from 'state'
|
||||
import { useSelector } from 'state'
|
||||
import ColorContent from './color-content'
|
||||
|
||||
export default function QuickColorSelect(): JSX.Element {
|
||||
|
@ -11,17 +11,12 @@ export default function QuickColorSelect(): JSX.Element {
|
|||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
>
|
||||
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
|
||||
<Tooltip label="Color">
|
||||
<Square fill={strokes[color]} stroke={strokes[color]} />
|
||||
</Tooltip>
|
||||
</DropdownMenu.Trigger>
|
||||
<ColorContent
|
||||
onChange={(color) => state.send('CHANGED_STYLE', { color })}
|
||||
/>
|
||||
<ColorContent />
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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 { memo } from 'react'
|
||||
import state, { useSelector } from 'state'
|
||||
import { DashStyle } from 'types'
|
||||
import {
|
||||
|
@ -17,40 +18,35 @@ const dashes = {
|
|||
[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)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
>
|
||||
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
|
||||
<Tooltip label="Dash">{dashes[dash]}</Tooltip>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownContent sideOffset={8} direction="vertical">
|
||||
<DashItem isActive={dash === DashStyle.Solid} dash={DashStyle.Solid} />
|
||||
<DashItem
|
||||
isActive={dash === DashStyle.Dashed}
|
||||
dash={DashStyle.Dashed}
|
||||
/>
|
||||
<DashItem
|
||||
isActive={dash === DashStyle.Dotted}
|
||||
dash={DashStyle.Dotted}
|
||||
/>
|
||||
{Object.keys(DashStyle).map((dashStyle: DashStyle) => (
|
||||
<DropdownMenu.DropdownMenuItem
|
||||
as={Item}
|
||||
key={dashStyle}
|
||||
isActive={dash === dashStyle}
|
||||
onSelect={changeDashStyle}
|
||||
value={dashStyle}
|
||||
>
|
||||
{dashes[dashStyle]}
|
||||
</DropdownMenu.DropdownMenuItem>
|
||||
))}
|
||||
</DropdownContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function DashItem({ dash, isActive }: { isActive: boolean; dash: DashStyle }) {
|
||||
return (
|
||||
<Item
|
||||
as={DropdownMenu.DropdownMenuItem}
|
||||
isActive={isActive}
|
||||
onSelect={() => state.send('CHANGED_STYLE', { dash })}
|
||||
>
|
||||
{dashes[dash]}
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
export default memo(QuickdashSelect)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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 { memo } from 'react'
|
||||
import { Circle } from 'react-feather'
|
||||
import state, { useSelector } from 'state'
|
||||
import { SizeStyle } from 'types'
|
||||
|
@ -12,39 +13,37 @@ const sizes = {
|
|||
[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)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||
>
|
||||
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
|
||||
<Tooltip label="Size">
|
||||
<Circle size={sizes[size]} stroke="none" fill="currentColor" />
|
||||
</Tooltip>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownContent sideOffset={8} direction="vertical">
|
||||
<SizeItem isActive={size === SizeStyle.Small} size={SizeStyle.Small} />
|
||||
<SizeItem
|
||||
isActive={size === SizeStyle.Medium}
|
||||
size={SizeStyle.Medium}
|
||||
/>
|
||||
<SizeItem isActive={size === SizeStyle.Large} size={SizeStyle.Large} />
|
||||
{Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => (
|
||||
<DropdownMenu.DropdownMenuItem
|
||||
key={sizeStyle}
|
||||
as={Item}
|
||||
isActive={size === sizeStyle}
|
||||
value={sizeStyle}
|
||||
onSelect={handleSizeChange}
|
||||
>
|
||||
<Circle size={sizes[sizeStyle]} />
|
||||
</DropdownMenu.DropdownMenuItem>
|
||||
))}
|
||||
</DropdownContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function SizeItem({ size, isActive }: { isActive: boolean; size: SizeStyle }) {
|
||||
return (
|
||||
<Item
|
||||
as={DropdownMenu.DropdownMenuItem}
|
||||
isActive={isActive}
|
||||
onSelect={() => state.send('CHANGED_STYLE', { size })}
|
||||
>
|
||||
<Circle size={sizes[size]} />
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
export default memo(QuickSizeSelect)
|
||||
|
|
|
@ -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 * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import { Circle } from 'react-feather'
|
||||
import state from 'state'
|
||||
import state, { useSelector } from 'state'
|
||||
import { SizeStyle } from 'types'
|
||||
import { memo } from 'react'
|
||||
|
||||
const sizes = {
|
||||
[SizeStyle.Small]: 6,
|
||||
[SizeStyle.Medium]: 12,
|
||||
[SizeStyle.Large]: 22,
|
||||
}
|
||||
|
||||
function handleChange(size: string) {
|
||||
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 (
|
||||
<Group name="width" onValueChange={handleChange}>
|
||||
<Item
|
||||
as={RadioGroup.Item}
|
||||
value={SizeStyle.Small}
|
||||
isActive={size === SizeStyle.Small}
|
||||
>
|
||||
<Circle size={6} />
|
||||
</Item>
|
||||
<Item
|
||||
as={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>
|
||||
{Object.keys(SizeStyle).map((sizeStyle: SizeStyle) => (
|
||||
<RadioGroup.Item
|
||||
key={sizeStyle}
|
||||
as={Item}
|
||||
isActive={size === sizeStyle}
|
||||
value={sizeStyle}
|
||||
>
|
||||
<Circle size={sizes[sizeStyle]} />
|
||||
</RadioGroup.Item>
|
||||
))}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SizePicker)
|
||||
|
|
|
@ -3,31 +3,10 @@ import state, { useSelector } from 'state'
|
|||
import * as Panel from 'components/panel'
|
||||
import { useRef } from 'react'
|
||||
import { IconButton } from 'components/shared'
|
||||
import { ChevronDown, Trash2, X } from 'react-feather'
|
||||
import {
|
||||
deepCompare,
|
||||
deepCompareArrays,
|
||||
getPage,
|
||||
getSelectedIds,
|
||||
setToArray,
|
||||
} from 'utils'
|
||||
import { ChevronDown, X } from 'react-feather'
|
||||
import ShapesFunctions from './shapes-functions'
|
||||
import AlignDistribute from './align-distribute'
|
||||
import { MoveType } from 'types'
|
||||
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 QuickColorSelect from './quick-color-select'
|
||||
import ColorPicker from './color-picker'
|
||||
|
@ -37,23 +16,12 @@ import QuickdashSelect from './quick-dash-select'
|
|||
import Tooltip from 'components/tooltip'
|
||||
|
||||
const breakpoints = { '@initial': 'mobile', '@sm': 'small' } as any
|
||||
|
||||
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 {
|
||||
const rContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
|
||||
|
||||
return (
|
||||
|
@ -86,29 +54,7 @@ export default function StylePanel(): JSX.Element {
|
|||
// track of this data manually within our state.
|
||||
|
||||
function SelectedShapeStyles(): JSX.Element {
|
||||
const selectedIds = useSelector(
|
||||
(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
|
||||
const selectedShapesCount = useSelector((s) => s.values.selectedIds.length)
|
||||
|
||||
return (
|
||||
<Panel.Layout>
|
||||
|
@ -123,133 +69,20 @@ function SelectedShapeStyles(): JSX.Element {
|
|||
</IconButton>
|
||||
</Panel.Header>
|
||||
<Content>
|
||||
<ColorPicker color={commonStyle.color} onChange={handleColorChange} />
|
||||
<IsFilledPicker
|
||||
isFilled={commonStyle.isFilled}
|
||||
onChange={handleIsFilledChange}
|
||||
/>
|
||||
<ColorPicker />
|
||||
<IsFilledPicker />
|
||||
<Row>
|
||||
<label htmlFor="size">Size</label>
|
||||
<SizePicker size={commonStyle.size} />
|
||||
<SizePicker />
|
||||
</Row>
|
||||
<Row>
|
||||
<label htmlFor="dash">Dash</label>
|
||||
<DashPicker dash={commonStyle.dash} />
|
||||
<DashPicker />
|
||||
</Row>
|
||||
<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>
|
||||
<ShapesFunctions />
|
||||
<AlignDistribute
|
||||
hasTwoOrMore={selectedIds.length > 1}
|
||||
hasThreeOrMore={selectedIds.length > 2}
|
||||
hasTwoOrMore={selectedShapesCount > 1}
|
||||
hasThreeOrMore={selectedShapesCount > 2}
|
||||
/>
|
||||
</Content>
|
||||
</Panel.Layout>
|
||||
|
@ -306,16 +139,3 @@ const Row = styled('div', {
|
|||
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
|
||||
return useSelector((s) => {
|
||||
const visiblePageShapeIds = useSelector((s) => {
|
||||
const pageState = getPageState(s.data)
|
||||
|
||||
if (!viewportCache.has(pageState)) {
|
||||
|
@ -46,4 +46,6 @@ export default function usePageShapes(): string[] {
|
|||
})
|
||||
.map((shape) => shape.id)
|
||||
}, deepCompareArrays)
|
||||
|
||||
return visiblePageShapeIds
|
||||
}
|
||||
|
|
|
@ -1870,9 +1870,17 @@ const state = createState({
|
|||
data.boundsRotation = 0
|
||||
},
|
||||
},
|
||||
asyncs: {
|
||||
async getUpdatedShapes(data) {
|
||||
return updateFromCode(
|
||||
data,
|
||||
data.document.code[data.currentCodeFileId].code
|
||||
)
|
||||
},
|
||||
},
|
||||
values: {
|
||||
selectedIds(data) {
|
||||
return new Set(getSelectedIds(data))
|
||||
return setToArray(getSelectedIds(data))
|
||||
},
|
||||
selectedBounds(data) {
|
||||
return getSelectionBounds(data)
|
||||
|
@ -1915,14 +1923,6 @@ const state = createState({
|
|||
return commonStyle
|
||||
},
|
||||
},
|
||||
asyncs: {
|
||||
async getUpdatedShapes(data) {
|
||||
return updateFromCode(
|
||||
data,
|
||||
data.document.code[data.currentCodeFileId].code
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default state
|
||||
|
|
Ładowanie…
Reference in New Issue