Adds page control, pages
This commit is contained in:
parent
34256f992a
commit
5ba56216d0
26 changed files with 460 additions and 295 deletions
|
@ -1,8 +1,9 @@
|
|||
import * as React from 'react'
|
||||
import { Edge, Corner, LineShape, ArrowShape } from 'types'
|
||||
import { Edge, Corner } from 'types'
|
||||
import { useSelector } from 'state'
|
||||
import {
|
||||
deepCompareArrays,
|
||||
getCurrentCamera,
|
||||
getPage,
|
||||
getSelectedShapes,
|
||||
isMobile,
|
||||
|
@ -17,7 +18,7 @@ import Handles from './handles'
|
|||
export default function Bounds() {
|
||||
const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
|
||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||
const zoom = useSelector((s) => s.data.camera.zoom)
|
||||
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
|
||||
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||
|
||||
const selectedIds = useSelector(
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import { memo } from 'react'
|
||||
import { useSelector } from 'state'
|
||||
import { deepCompareArrays, getPage } from 'utils/utils'
|
||||
import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
|
||||
|
||||
export default function Defs() {
|
||||
const zoom = useSelector((s) => s.data.camera.zoom)
|
||||
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
|
||||
|
||||
const currentPageShapeIds = useSelector(({ data }) => {
|
||||
return Object.values(getPage(data).shapes)
|
||||
|
|
|
@ -8,6 +8,7 @@ import ToolsPanel from './tools-panel/tools-panel'
|
|||
import StylePanel from './style-panel/style-panel'
|
||||
import { useSelector } from 'state'
|
||||
import styled from 'styles'
|
||||
import PagePanel from './page-panel/page-panel'
|
||||
|
||||
export default function Editor() {
|
||||
useKeyboardEvents()
|
||||
|
@ -20,6 +21,7 @@ export default function Editor() {
|
|||
return (
|
||||
<Layout>
|
||||
<Canvas />
|
||||
<PagePanel />
|
||||
<LeftPanels>
|
||||
<CodePanel />
|
||||
{hasControls && <ControlsPanel />}
|
||||
|
|
100
components/page-panel/page-panel.tsx
Normal file
100
components/page-panel/page-panel.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import styled from 'styles'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import { IconWrapper, RowButton } from 'components/shared'
|
||||
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons'
|
||||
import * as Panel from '../panel'
|
||||
import state, { useSelector } from 'state'
|
||||
import { getPage } from 'utils/utils'
|
||||
|
||||
export default function PagePanel() {
|
||||
const currentPageId = useSelector((s) => s.data.currentPageId)
|
||||
const documentPages = useSelector((s) => s.data.document.pages)
|
||||
|
||||
const sorted = Object.values(documentPages).sort(
|
||||
(a, b) => a.childIndex - b.childIndex
|
||||
)
|
||||
|
||||
return (
|
||||
<OuterContainer>
|
||||
<DropdownMenu.Root>
|
||||
<PanelRoot>
|
||||
<DropdownMenu.Trigger as={RowButton}>
|
||||
<span>{documentPages[currentPageId].name}</span>
|
||||
<IconWrapper size="small">
|
||||
<ChevronDownIcon />
|
||||
</IconWrapper>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content sideOffset={8}>
|
||||
<PanelRoot>
|
||||
<DropdownMenu.RadioGroup
|
||||
as={Content}
|
||||
value={currentPageId}
|
||||
onValueChange={(id) =>
|
||||
state.send('CHANGED_CURRENT_PAGE', { id })
|
||||
}
|
||||
>
|
||||
{sorted.map(({ id, name }) => (
|
||||
<StyledRadioItem key={id} value={id}>
|
||||
<span>{name}</span>
|
||||
<DropdownMenu.ItemIndicator as={IconWrapper} size="small">
|
||||
<CheckIcon />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</StyledRadioItem>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
</PanelRoot>
|
||||
</DropdownMenu.Content>
|
||||
</PanelRoot>
|
||||
</DropdownMenu.Root>
|
||||
</OuterContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const PanelRoot = styled('div', {
|
||||
minWidth: 1,
|
||||
width: 184,
|
||||
maxWidth: 184,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'all',
|
||||
padding: '2px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
})
|
||||
|
||||
const Content = styled(Panel.Content, {
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const StyledRadioItem = styled(DropdownMenu.RadioItem, {
|
||||
height: 32,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 6px 0 12px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
outline: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
})
|
||||
|
||||
const OuterContainer = styled('div', {
|
||||
position: 'fixed',
|
||||
top: 8,
|
||||
left: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
zIndex: 200,
|
||||
height: 44,
|
||||
})
|
|
@ -1,3 +1,6 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import * as Panel from './panel'
|
||||
import styled from 'styles'
|
||||
|
||||
export const IconButton = styled('button', {
|
||||
|
@ -60,3 +63,236 @@ export const IconButton = styled('button', {
|
|||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const RowButton = styled('button', {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
background: 'none',
|
||||
height: '32px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '4px 6px 4px 12px',
|
||||
|
||||
'&::before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'none',
|
||||
zIndex: -1,
|
||||
},
|
||||
|
||||
'&:hover::before': {
|
||||
backgroundColor: '$hover',
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
'& label': {
|
||||
fontFamily: '$ui',
|
||||
fontSize: '$2',
|
||||
fontWeight: '$1',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
},
|
||||
|
||||
'& svg': {
|
||||
position: 'relative',
|
||||
stroke: 'rgba(0,0,0,.2)',
|
||||
strokeWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
icon: {
|
||||
padding: '4px ',
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const StylePanelRoot = styled(Panel.Root, {
|
||||
minWidth: 1,
|
||||
width: 184,
|
||||
maxWidth: 184,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
|
||||
|
||||
variants: {
|
||||
isOpen: {
|
||||
true: {},
|
||||
false: {
|
||||
padding: 2,
|
||||
height: 38,
|
||||
width: 38,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const Group = styled(RadioGroup.Root, {
|
||||
display: 'flex',
|
||||
})
|
||||
|
||||
export const Item = styled('button', {
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '4px',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
|
||||
'&:hover:not(:disabled)': {
|
||||
backgroundColor: '$hover',
|
||||
'& svg': {
|
||||
stroke: '$text',
|
||||
fill: '$text',
|
||||
strokeWidth: '0',
|
||||
},
|
||||
},
|
||||
|
||||
'&:disabled': {
|
||||
opacity: '0.5',
|
||||
},
|
||||
|
||||
variants: {
|
||||
isActive: {
|
||||
true: {
|
||||
'& svg': {
|
||||
fill: '$text',
|
||||
stroke: '$text',
|
||||
},
|
||||
},
|
||||
false: {
|
||||
'& svg': {
|
||||
fill: '$inactive',
|
||||
stroke: '$inactive',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const IconWrapper = styled('div', {
|
||||
height: '100%',
|
||||
borderRadius: '4px',
|
||||
marginRight: '1px',
|
||||
display: 'grid',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
|
||||
'& svg': {
|
||||
height: 22,
|
||||
width: 22,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
|
||||
'& > *': {
|
||||
gridRow: 1,
|
||||
gridColumn: 1,
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
small: {
|
||||
'& svg': {
|
||||
height: '16px',
|
||||
width: '16px',
|
||||
},
|
||||
},
|
||||
medium: {
|
||||
'& svg': {
|
||||
height: '22px',
|
||||
width: '22px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const DropdownContent = styled(DropdownMenu.Content, {
|
||||
display: 'grid',
|
||||
padding: 4,
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: 4,
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.28)',
|
||||
|
||||
variants: {
|
||||
direction: {
|
||||
vertical: {
|
||||
gridTemplateColumns: '1fr',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function DashSolidIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DashDashedIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={50.26548 * 0.1}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
|
||||
|
||||
export function DashDottedIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={dottedDasharray}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ColorStyle } from 'types'
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { Square } from 'react-feather'
|
||||
import styled from 'styles'
|
||||
import { DropdownContent } from './shared'
|
||||
import { DropdownContent } from '../shared'
|
||||
|
||||
export default function ColorContent({
|
||||
onChange,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { strokes } from 'lib/shape-styles'
|
||||
import { ColorStyle } from 'types'
|
||||
import { IconWrapper, RowButton } from './shared'
|
||||
import { RowButton, IconWrapper } from '../shared'
|
||||
import { Square } from 'react-feather'
|
||||
import ColorContent from './color-content'
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
DashDashedIcon,
|
||||
DashDottedIcon,
|
||||
DashSolidIcon,
|
||||
} from './shared'
|
||||
} from '../shared'
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import { DashStyle } from 'types'
|
||||
import state from 'state'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as Checkbox from '@radix-ui/react-checkbox'
|
|||
import { CheckIcon } from '@radix-ui/react-icons'
|
||||
import { strokes } from 'lib/shape-styles'
|
||||
import { Square } from 'react-feather'
|
||||
import { IconWrapper, RowButton } from './shared'
|
||||
import { IconWrapper, RowButton } from '../shared'
|
||||
|
||||
interface Props {
|
||||
isFilled: boolean
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
DashDottedIcon,
|
||||
DashSolidIcon,
|
||||
DashDashedIcon,
|
||||
} from './shared'
|
||||
} from '../shared'
|
||||
|
||||
const dashes = {
|
||||
[DashStyle.Solid]: <DashSolidIcon />,
|
||||
|
|
|
@ -4,7 +4,7 @@ import Tooltip from 'components/tooltip'
|
|||
import { Circle } from 'react-feather'
|
||||
import state, { useSelector } from 'state'
|
||||
import { SizeStyle } from 'types'
|
||||
import { DropdownContent, Item } from './shared'
|
||||
import { DropdownContent, Item } from '../shared'
|
||||
|
||||
const sizes = {
|
||||
[SizeStyle.Small]: 6,
|
||||
|
|
|
@ -1,219 +0,0 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import * as Panel from '../panel'
|
||||
import styled from 'styles'
|
||||
|
||||
export const StylePanelRoot = styled(Panel.Root, {
|
||||
minWidth: 1,
|
||||
width: 184,
|
||||
maxWidth: 184,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
|
||||
|
||||
variants: {
|
||||
isOpen: {
|
||||
true: {},
|
||||
false: {
|
||||
padding: 2,
|
||||
height: 38,
|
||||
width: 38,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const Group = styled(RadioGroup.Root, {
|
||||
display: 'flex',
|
||||
})
|
||||
|
||||
export const Item = styled('button', {
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '4px',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
|
||||
'&:hover:not(:disabled)': {
|
||||
backgroundColor: '$hover',
|
||||
'& svg': {
|
||||
stroke: '$text',
|
||||
fill: '$text',
|
||||
strokeWidth: '0',
|
||||
},
|
||||
},
|
||||
|
||||
'&:disabled': {
|
||||
opacity: '0.5',
|
||||
},
|
||||
|
||||
variants: {
|
||||
isActive: {
|
||||
true: {
|
||||
'& svg': {
|
||||
fill: '$text',
|
||||
stroke: '$text',
|
||||
},
|
||||
},
|
||||
false: {
|
||||
'& svg': {
|
||||
fill: '$inactive',
|
||||
stroke: '$inactive',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const RowButton = styled('button', {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '4px 6px 4px 12px',
|
||||
|
||||
'&::before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'none',
|
||||
zIndex: -1,
|
||||
},
|
||||
|
||||
'&:hover::before': {
|
||||
backgroundColor: '$hover',
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
'& label': {
|
||||
fontFamily: '$ui',
|
||||
fontSize: '$2',
|
||||
fontWeight: '$1',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
},
|
||||
|
||||
'& svg': {
|
||||
position: 'relative',
|
||||
stroke: 'rgba(0,0,0,.2)',
|
||||
strokeWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
icon: {
|
||||
padding: '4px ',
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const IconWrapper = styled('div', {
|
||||
height: '100%',
|
||||
borderRadius: '4px',
|
||||
marginRight: '1px',
|
||||
display: 'grid',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
|
||||
'& svg': {
|
||||
height: 22,
|
||||
width: 22,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
|
||||
'& > *': {
|
||||
gridRow: 1,
|
||||
gridColumn: 1,
|
||||
},
|
||||
})
|
||||
|
||||
export const DropdownContent = styled(DropdownMenu.Content, {
|
||||
display: 'grid',
|
||||
padding: 4,
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: 4,
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.28)',
|
||||
|
||||
variants: {
|
||||
direction: {
|
||||
vertical: {
|
||||
gridTemplateColumns: '1fr',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function DashSolidIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DashDashedIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={50.26548 * 0.1}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
|
||||
|
||||
export function DashDottedIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={dottedDasharray}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Group, Item } from './shared'
|
||||
import { Group, Item } from '../shared'
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import { ChangeEvent } from 'react'
|
||||
import { Circle } from 'react-feather'
|
||||
|
|
|
@ -3,10 +3,8 @@ import state, { useSelector } from 'state'
|
|||
import * as Panel from 'components/panel'
|
||||
import { useRef } from 'react'
|
||||
import { IconButton } from 'components/shared'
|
||||
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||
import { ChevronDown, Square, Tool, Trash2, X } from 'react-feather'
|
||||
import { ChevronDown, Trash2, X } from 'react-feather'
|
||||
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
|
||||
import { strokes } from 'lib/shape-styles'
|
||||
import AlignDistribute from './align-distribute'
|
||||
import { MoveType } from 'types'
|
||||
import SizePicker from './size-picker'
|
||||
|
@ -15,9 +13,7 @@ import {
|
|||
ArrowUpIcon,
|
||||
AspectRatioIcon,
|
||||
BoxIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
DotsVerticalIcon,
|
||||
EyeClosedIcon,
|
||||
EyeOpenIcon,
|
||||
LockClosedIcon,
|
||||
|
@ -28,10 +24,7 @@ import {
|
|||
} from '@radix-ui/react-icons'
|
||||
import DashPicker from './dash-picker'
|
||||
import QuickColorSelect from './quick-color-select'
|
||||
import ColorContent from './color-content'
|
||||
import { RowButton, IconWrapper } from './shared'
|
||||
import ColorPicker from './color-picker'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import IsFilledPicker from './is-filled-picker'
|
||||
import QuickSizeSelect from './quick-size-select'
|
||||
import QuickdashSelect from './quick-dash-select'
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ZoomInIcon, ZoomOutIcon } from '@radix-ui/react-icons'
|
|||
import { IconButton } from 'components/shared'
|
||||
import state, { useSelector } from 'state'
|
||||
import styled from 'styles'
|
||||
import { getCurrentCamera } from 'utils/utils'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
const zoomIn = () => state.send('ZOOMED_IN')
|
||||
|
@ -30,10 +31,11 @@ export default function Zoom() {
|
|||
}
|
||||
|
||||
function ZoomCounter() {
|
||||
const camera = useSelector((s) => s.data.camera)
|
||||
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
|
||||
|
||||
return (
|
||||
<ZoomButton onClick={zoomToActual} onDoubleClick={zoomToFit}>
|
||||
{Math.round(camera.zoom * 100)}%
|
||||
{Math.round(zoom * 100)}%
|
||||
</ZoomButton>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect } from "react"
|
||||
import state from "state"
|
||||
import React, { useEffect } from 'react'
|
||||
import state from 'state'
|
||||
import { getCurrentCamera } from 'utils/utils'
|
||||
|
||||
/**
|
||||
* When the state's camera changes, update the transform of
|
||||
|
@ -8,24 +9,27 @@ import state from "state"
|
|||
*/
|
||||
export default function useCamera(ref: React.MutableRefObject<SVGGElement>) {
|
||||
useEffect(() => {
|
||||
let { camera } = state.data
|
||||
let prev = getCurrentCamera(state.data)
|
||||
|
||||
return state.onUpdate(({ data }) => {
|
||||
return state.onUpdate(() => {
|
||||
const g = ref.current
|
||||
if (!g) return
|
||||
|
||||
const { point, zoom } = data.camera
|
||||
const { point, zoom } = getCurrentCamera(state.data)
|
||||
|
||||
if (point !== camera.point || zoom !== camera.zoom) {
|
||||
if (point !== prev.point || zoom !== prev.zoom) {
|
||||
g.setAttribute(
|
||||
"transform",
|
||||
'transform',
|
||||
`scale(${zoom}) translate(${point[0]} ${point[1]})`
|
||||
)
|
||||
|
||||
localStorage.setItem("code_slate_camera", JSON.stringify(data.camera))
|
||||
}
|
||||
localStorage.setItem(
|
||||
'code_slate_camera',
|
||||
JSON.stringify({ point, zoom })
|
||||
)
|
||||
|
||||
camera = data.camera
|
||||
prev = getCurrentCamera(state.data)
|
||||
}
|
||||
})
|
||||
}, [state])
|
||||
}
|
||||
|
|
24
state/commands/change-page.ts
Normal file
24
state/commands/change-page.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data } from 'types'
|
||||
import { getPage, getSelectedShapes } from 'utils/utils'
|
||||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import * as vec from 'utils/vec'
|
||||
|
||||
export default function nudgeCommand(data: Data, pageId: string) {
|
||||
const { currentPageId: prevPageId } = data
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
name: 'change_page',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
data.currentPageId = pageId
|
||||
},
|
||||
undo(data) {
|
||||
data.currentPageId = prevPageId
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data } from 'types'
|
||||
import { getPage, getSelectedShapes } from 'utils/utils'
|
||||
import { getCurrentCamera, getPage, getSelectedShapes } from 'utils/utils'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { current } from 'immer'
|
||||
import * as vec from 'utils/vec'
|
||||
|
@ -12,7 +12,7 @@ export default function duplicateCommand(data: Data) {
|
|||
const duplicates = selectedShapes.map((shape) => ({
|
||||
...shape,
|
||||
id: uuid(),
|
||||
point: vec.add(shape.point, vec.div([16, 16], data.camera.zoom)),
|
||||
point: vec.add(shape.point, vec.div([16, 16], getCurrentCamera(data).zoom)),
|
||||
}))
|
||||
|
||||
history.execute(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import align from './align'
|
||||
import arrow from './arrow'
|
||||
import changePage from './change-page'
|
||||
import deleteSelected from './delete-selected'
|
||||
import direct from './direct'
|
||||
import distribute from './distribute'
|
||||
|
@ -21,6 +22,7 @@ import handle from './handle'
|
|||
const commands = {
|
||||
align,
|
||||
arrow,
|
||||
changePage,
|
||||
deleteSelected,
|
||||
direct,
|
||||
distribute,
|
||||
|
|
|
@ -10,8 +10,6 @@ export default function transformSingleCommand(
|
|||
data: Data,
|
||||
before: TransformSingleSnapshot,
|
||||
after: TransformSingleSnapshot,
|
||||
scaleX: number,
|
||||
scaleY: number,
|
||||
isCreating: boolean
|
||||
) {
|
||||
const shape = current(getPage(data, after.currentPageId).shapes[after.id])
|
||||
|
@ -23,24 +21,14 @@ export default function transformSingleCommand(
|
|||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const { id, type, initialShapeBounds } = after
|
||||
const { id } = after
|
||||
|
||||
const { shapes } = getPage(data, after.currentPageId)
|
||||
|
||||
data.selectedIds.clear()
|
||||
data.selectedIds.add(id)
|
||||
|
||||
if (isCreating) {
|
||||
shapes[id] = shape
|
||||
} else {
|
||||
getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
|
||||
type,
|
||||
initialShape: before.initialShape,
|
||||
scaleX,
|
||||
scaleY,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
})
|
||||
}
|
||||
shapes[id] = shape
|
||||
},
|
||||
undo(data) {
|
||||
const { id, type, initialShapeBounds } = before
|
||||
|
|
|
@ -128,6 +128,13 @@ export const defaultDocument: Data['document'] = {
|
|||
// }),
|
||||
},
|
||||
},
|
||||
page1: {
|
||||
id: 'page1',
|
||||
type: 'page',
|
||||
name: 'Page 1',
|
||||
childIndex: 1,
|
||||
shapes: {},
|
||||
},
|
||||
},
|
||||
code: {
|
||||
file0: {
|
||||
|
|
|
@ -106,7 +106,7 @@ class History extends BaseHistory<Data> {
|
|||
}
|
||||
|
||||
restoreSavedData(data: any): Data {
|
||||
const restoredData = { ...data }
|
||||
const restoredData: Data = { ...data }
|
||||
|
||||
restoredData.selectedIds = new Set(restoredData.selectedIds)
|
||||
|
||||
|
@ -114,12 +114,15 @@ class History extends BaseHistory<Data> {
|
|||
const cameraInfo = localStorage.getItem('code_slate_camera')
|
||||
|
||||
if (cameraInfo !== null) {
|
||||
Object.assign(restoredData.camera, JSON.parse(cameraInfo))
|
||||
Object.assign(
|
||||
restoredData.pageStates[data.currentPageId].camera,
|
||||
JSON.parse(cameraInfo)
|
||||
)
|
||||
|
||||
// And update the CSS property
|
||||
document.documentElement.style.setProperty(
|
||||
'--camera-zoom',
|
||||
restoredData.camera.zoom.toString()
|
||||
restoredData.pageStates[data.currentPageId].camera.zoom.toString()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -85,8 +85,6 @@ export default class TransformSingleSession extends BaseSession {
|
|||
data,
|
||||
this.snapshot,
|
||||
getTransformSingleSnapshot(data, this.transformType),
|
||||
this.scaleX,
|
||||
this.scaleY,
|
||||
this.isCreating
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
getChildren,
|
||||
getCommonBounds,
|
||||
getCurrent,
|
||||
getCurrentCamera,
|
||||
getPage,
|
||||
getSelectedBounds,
|
||||
getShape,
|
||||
|
@ -54,10 +55,6 @@ const initialData: Data = {
|
|||
dash: DashStyle.Solid,
|
||||
isFilled: false,
|
||||
},
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
activeTool: 'select',
|
||||
brush: undefined,
|
||||
boundsRotation: 0,
|
||||
|
@ -68,6 +65,20 @@ const initialData: Data = {
|
|||
currentCodeFileId: 'file0',
|
||||
codeControls: {},
|
||||
document: defaultDocument,
|
||||
pageStates: {
|
||||
page0: {
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
},
|
||||
page1: {
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const state = createState({
|
||||
|
@ -139,6 +150,7 @@ const state = createState({
|
|||
USED_PEN_DEVICE: 'enablePenLock',
|
||||
DISABLED_PEN_LOCK: 'disablePenLock',
|
||||
CLEARED_PAGE: ['selectAll', 'deleteSelection'],
|
||||
CHANGED_CURRENT_PAGE: ['clearSelectedIds', 'setCurrentPage'],
|
||||
},
|
||||
initial: 'selecting',
|
||||
states: {
|
||||
|
@ -732,6 +744,11 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
/* ---------------------- Pages --------------------- */
|
||||
setCurrentPage(data, payload: { id: string }) {
|
||||
commands.changePage(data, payload.id)
|
||||
},
|
||||
|
||||
/* --------------------- Shapes --------------------- */
|
||||
createShape(data, payload, type: ShapeType) {
|
||||
const shape = createShape(type, {
|
||||
|
@ -1062,7 +1079,7 @@ const state = createState({
|
|||
/* --------------------- Camera --------------------- */
|
||||
|
||||
zoomIn(data) {
|
||||
const { camera } = data
|
||||
const camera = getCurrentCamera(data)
|
||||
const i = Math.round((camera.zoom * 100) / 25)
|
||||
const center = [window.innerWidth / 2, window.innerHeight / 2]
|
||||
|
||||
|
@ -1074,7 +1091,7 @@ const state = createState({
|
|||
setZoomCSS(camera.zoom)
|
||||
},
|
||||
zoomOut(data) {
|
||||
const { camera } = data
|
||||
const camera = getCurrentCamera(data)
|
||||
const i = Math.round((camera.zoom * 100) / 25)
|
||||
const center = [window.innerWidth / 2, window.innerHeight / 2]
|
||||
|
||||
|
@ -1086,8 +1103,7 @@ const state = createState({
|
|||
setZoomCSS(camera.zoom)
|
||||
},
|
||||
zoomCameraToActual(data) {
|
||||
const { camera } = data
|
||||
|
||||
const camera = getCurrentCamera(data)
|
||||
const center = [window.innerWidth / 2, window.innerHeight / 2]
|
||||
|
||||
const p0 = screenToWorld(center, data)
|
||||
|
@ -1098,7 +1114,7 @@ const state = createState({
|
|||
setZoomCSS(camera.zoom)
|
||||
},
|
||||
zoomCameraToSelectionActual(data) {
|
||||
const { camera } = data
|
||||
const camera = getCurrentCamera(data)
|
||||
|
||||
const bounds = getSelectedBounds(data)
|
||||
|
||||
|
@ -1111,8 +1127,7 @@ const state = createState({
|
|||
setZoomCSS(camera.zoom)
|
||||
},
|
||||
zoomCameraToSelection(data) {
|
||||
const { camera } = data
|
||||
|
||||
const camera = getCurrentCamera(data)
|
||||
const bounds = getSelectedBounds(data)
|
||||
|
||||
const zoom = getCameraZoom(
|
||||
|
@ -1130,7 +1145,7 @@ const state = createState({
|
|||
setZoomCSS(camera.zoom)
|
||||
},
|
||||
zoomCameraToFit(data) {
|
||||
const { camera } = data
|
||||
const camera = getCurrentCamera(data)
|
||||
const page = getPage(data)
|
||||
|
||||
const shapes = Object.values(page.shapes)
|
||||
|
@ -1160,7 +1175,7 @@ const state = createState({
|
|||
setZoomCSS(camera.zoom)
|
||||
},
|
||||
zoomCamera(data, payload: { delta: number; point: number[] }) {
|
||||
const { camera } = data
|
||||
const camera = getCurrentCamera(data)
|
||||
const next = camera.zoom - (payload.delta / 100) * camera.zoom
|
||||
|
||||
const p0 = screenToWorld(payload.point, data)
|
||||
|
@ -1171,7 +1186,7 @@ const state = createState({
|
|||
setZoomCSS(camera.zoom)
|
||||
},
|
||||
panCamera(data, payload: { delta: number[] }) {
|
||||
const { camera } = data
|
||||
const camera = getCurrentCamera(data)
|
||||
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
|
||||
},
|
||||
pinchCamera(
|
||||
|
@ -1183,8 +1198,7 @@ const state = createState({
|
|||
point: number[]
|
||||
}
|
||||
) {
|
||||
const { camera } = data
|
||||
|
||||
const camera = getCurrentCamera(data)
|
||||
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
|
||||
|
||||
const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
|
||||
|
@ -1197,9 +1211,9 @@ const state = createState({
|
|||
setZoomCSS(camera.zoom)
|
||||
},
|
||||
resetCamera(data) {
|
||||
data.camera.zoom = 1
|
||||
data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
|
||||
|
||||
const camera = getCurrentCamera(data)
|
||||
camera.zoom = 1
|
||||
camera.point = [window.innerWidth / 2, window.innerHeight / 2]
|
||||
document.documentElement.style.setProperty('--camera-zoom', '1')
|
||||
},
|
||||
|
||||
|
|
13
types.ts
13
types.ts
|
@ -19,10 +19,6 @@ export interface Data {
|
|||
isPenLocked: boolean
|
||||
}
|
||||
currentStyle: ShapeStyles
|
||||
camera: {
|
||||
point: number[]
|
||||
zoom: number
|
||||
}
|
||||
activeTool: ShapeType | 'select'
|
||||
brush?: Bounds
|
||||
boundsRotation: number
|
||||
|
@ -36,6 +32,15 @@ export interface Data {
|
|||
pages: Record<string, Page>
|
||||
code: Record<string, CodeFile>
|
||||
}
|
||||
pageStates: Record<
|
||||
string,
|
||||
{
|
||||
camera: {
|
||||
point: number[]
|
||||
zoom: number
|
||||
}
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
|
|
|
@ -6,7 +6,8 @@ import _isMobile from 'ismobilejs'
|
|||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
|
||||
export function screenToWorld(point: number[], data: Data) {
|
||||
return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
|
||||
const camera = getCurrentCamera(data)
|
||||
return vec.sub(vec.div(point, camera.zoom), camera.point)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1581,3 +1582,7 @@ export function isAngleBetween(a: number, b: number, c: number) {
|
|||
const AC = (c - a + PI2) % PI2
|
||||
return AB <= Math.PI !== AC > AB
|
||||
}
|
||||
|
||||
export function getCurrentCamera(data: Data) {
|
||||
return data.pageStates[data.currentPageId].camera
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue