Updates style panel
This commit is contained in:
parent
04efb6f880
commit
39b943248f
22 changed files with 421 additions and 408 deletions
|
@ -80,11 +80,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.ToBack,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '1', '2', '4'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '1', '2', '4'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings to back', () => {
|
||||
|
@ -92,11 +88,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.ToBack,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '4', '3', '1'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '4', '3', '1'])
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings to back', () => {
|
||||
|
@ -104,11 +96,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.ToBack,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '1', '2', '3'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '1', '2', '3'])
|
||||
})
|
||||
|
||||
it('moves a shape backward', () => {
|
||||
|
@ -116,11 +104,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.Backward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '1', '3', '2'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '1', '3', '2'])
|
||||
})
|
||||
|
||||
it('moves a shape at first index backward', () => {
|
||||
|
@ -128,11 +112,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.Backward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '1', '3', '2'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '1', '3', '2'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings backward', () => {
|
||||
|
@ -140,23 +120,15 @@ describe('shapes with children', () => {
|
|||
type: MoveType.Backward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '3', '2', '1'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '3', '2', '1'])
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings backward', () => {
|
||||
tt.clickShape('3').clickShape('1', { shiftKey: true }).send('MOVED', {
|
||||
type: MoveType.Backward,
|
||||
})
|
||||
tt.clickShape('3')
|
||||
.clickShape('1', { shiftKey: true })
|
||||
.send('MOVED', { type: MoveType.Backward })
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '4', '1', '2'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '4', '1', '2'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings backward at zero index', () => {
|
||||
|
@ -164,11 +136,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.Backward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '4', '1', '2'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '4', '1', '2'])
|
||||
})
|
||||
|
||||
it('moves a shape forward', () => {
|
||||
|
@ -176,11 +144,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.Forward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '1', '4', '2'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '1', '4', '2'])
|
||||
})
|
||||
|
||||
it('moves a shape forward at the top index', () => {
|
||||
|
@ -188,11 +152,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.Forward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '1', '4', '2'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '1', '4', '2'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings forward', () => {
|
||||
|
@ -205,11 +165,7 @@ describe('shapes with children', () => {
|
|||
|
||||
expect(tt.idsAreSelected(['1', '4'])).toBe(true)
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '2', '1', '4'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '2', '1', '4'])
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings forward', () => {
|
||||
|
@ -220,11 +176,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.Forward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '3', '4', '1'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '3', '4', '1'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings forward at top index', () => {
|
||||
|
@ -234,11 +186,7 @@ describe('shapes with children', () => {
|
|||
.send('MOVED', {
|
||||
type: MoveType.Forward,
|
||||
})
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '4', '3', '1'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '4', '3', '1'])
|
||||
})
|
||||
|
||||
it('moves a shape to front', () => {
|
||||
|
@ -246,11 +194,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.ToFront,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '3', '1', '2'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '3', '1', '2'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings to front', () => {
|
||||
|
@ -261,11 +205,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.ToFront,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '2', '3', '1'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '2', '3', '1'])
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings to front', () => {
|
||||
|
@ -276,11 +216,7 @@ describe('shapes with children', () => {
|
|||
type: MoveType.ToFront,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '1', '4', '3'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '1', '4', '3'])
|
||||
})
|
||||
|
||||
it('moves siblings already at front to front', () => {
|
||||
|
@ -291,10 +227,6 @@ describe('shapes with children', () => {
|
|||
type: MoveType.ToFront,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '1', '4', '3'])
|
||||
expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '1', '4', '3'])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,32 +1,33 @@
|
|||
import { DashStyle } from 'types'
|
||||
import { getPerfectDashProps } from 'utils'
|
||||
|
||||
describe('ellipse dash props', () => {
|
||||
it('renders dashed props on a circle correctly', () => {
|
||||
expect(getPerfectDashProps(100, 4, 'dashed')).toMatchSnapshot(
|
||||
expect(getPerfectDashProps(100, 4, DashStyle.Dashed)).toMatchSnapshot(
|
||||
'small dashed circle dash props'
|
||||
)
|
||||
expect(getPerfectDashProps(100, 4, 'dashed')).toMatchSnapshot(
|
||||
expect(getPerfectDashProps(100, 4, DashStyle.Dashed)).toMatchSnapshot(
|
||||
'small dashed ellipse dash props'
|
||||
)
|
||||
expect(getPerfectDashProps(200, 8, 'dashed')).toMatchSnapshot(
|
||||
expect(getPerfectDashProps(200, 8, DashStyle.Dashed)).toMatchSnapshot(
|
||||
'large dashed circle dash props'
|
||||
)
|
||||
expect(getPerfectDashProps(200, 8, 'dashed')).toMatchSnapshot(
|
||||
expect(getPerfectDashProps(200, 8, DashStyle.Dashed)).toMatchSnapshot(
|
||||
'large dashed ellipse dash props'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders dotted props on a circle correctly', () => {
|
||||
expect(getPerfectDashProps(100, 4, 'dotted')).toMatchSnapshot(
|
||||
expect(getPerfectDashProps(100, 4, DashStyle.Dotted)).toMatchSnapshot(
|
||||
'small dotted circle dash props'
|
||||
)
|
||||
expect(getPerfectDashProps(100, 4, 'dotted')).toMatchSnapshot(
|
||||
expect(getPerfectDashProps(100, 4, DashStyle.Dotted)).toMatchSnapshot(
|
||||
'small dotted ellipse dash props'
|
||||
)
|
||||
expect(getPerfectDashProps(200, 8, 'dotted')).toMatchSnapshot(
|
||||
expect(getPerfectDashProps(200, 8, DashStyle.Dotted)).toMatchSnapshot(
|
||||
'large dotted circle dash props'
|
||||
)
|
||||
expect(getPerfectDashProps(200, 8, 'dotted')).toMatchSnapshot(
|
||||
expect(getPerfectDashProps(200, 8, DashStyle.Dotted)).toMatchSnapshot(
|
||||
'large dotted ellipse dash props'
|
||||
)
|
||||
})
|
||||
|
|
|
@ -91,6 +91,23 @@ class TestState {
|
|||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sorted ids of the page's children.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.getSortedPageShapes()
|
||||
*```
|
||||
*/
|
||||
getSortedPageShapeIds(): string[] {
|
||||
return Object.values(
|
||||
this.data.document.pages[this.data.currentParentId].shapes
|
||||
)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the provided ids are the current selected ids. If the `strict` argument is `true`, then the result will be false if the state has selected ids in addition to those provided.
|
||||
*
|
||||
|
|
|
@ -49,6 +49,7 @@ enum SizeStyle {
|
|||
}
|
||||
|
||||
enum DashStyle {
|
||||
Draw = 'Draw',
|
||||
Solid = 'Solid',
|
||||
Dashed = 'Dashed',
|
||||
Dotted = 'Dotted',
|
||||
|
@ -399,6 +400,9 @@ type PropsOfType<T extends Record<string, unknown>> = {
|
|||
type Mutable<T extends Shape> = { -readonly [K in keyof T]: T[K] }
|
||||
|
||||
interface ShapeUtility<K extends Shape> {
|
||||
// Default properties when creating a new shape
|
||||
defaultProps: K
|
||||
|
||||
// A cache for the computed bounds of this kind of shape.
|
||||
boundsCache: WeakMap<K, Bounds>
|
||||
|
||||
|
@ -424,7 +428,7 @@ interface ShapeUtility<K extends Shape> {
|
|||
isShy: boolean
|
||||
|
||||
// Create a new shape.
|
||||
create(props: Partial<K>): K
|
||||
create(this: ShapeUtility<K>, props: Partial<K>): K
|
||||
|
||||
// Update a shape's styles
|
||||
applyStyles(
|
||||
|
@ -612,6 +616,7 @@ enum SizeStyle {
|
|||
}
|
||||
|
||||
enum DashStyle {
|
||||
Draw = 'Draw',
|
||||
Solid = 'Solid',
|
||||
Dashed = 'Dashed',
|
||||
Dotted = 'Dotted',
|
||||
|
@ -962,6 +967,9 @@ type PropsOfType<T extends Record<string, unknown>> = {
|
|||
type Mutable<T extends Shape> = { -readonly [K in keyof T]: T[K] }
|
||||
|
||||
interface ShapeUtility<K extends Shape> {
|
||||
// Default properties when creating a new shape
|
||||
defaultProps: K
|
||||
|
||||
// A cache for the computed bounds of this kind of shape.
|
||||
boundsCache: WeakMap<K, Bounds>
|
||||
|
||||
|
@ -987,7 +995,7 @@ interface ShapeUtility<K extends Shape> {
|
|||
isShy: boolean
|
||||
|
||||
// Create a new shape.
|
||||
create(props: Partial<K>): K
|
||||
create(this: ShapeUtility<K>, props: Partial<K>): K
|
||||
|
||||
// Update a shape's styles
|
||||
applyStyles(
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -10,10 +10,9 @@ import {
|
|||
StretchHorizontallyIcon,
|
||||
StretchVerticallyIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { breakpoints, IconButton } from 'components/shared'
|
||||
import { breakpoints, ButtonsRow, IconButton } from 'components/shared'
|
||||
import { memo } from 'react'
|
||||
import state from 'state'
|
||||
import styled from 'styles'
|
||||
import { AlignType, DistributeType, StretchType } from 'types'
|
||||
|
||||
function alignTop() {
|
||||
|
@ -64,7 +63,8 @@ function AlignDistribute({
|
|||
hasThreeOrMore: boolean
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<>
|
||||
<ButtonsRow>
|
||||
<IconButton
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
|
@ -105,6 +105,8 @@ function AlignDistribute({
|
|||
>
|
||||
<SpaceEvenlyHorizontallyIcon />
|
||||
</IconButton>
|
||||
</ButtonsRow>
|
||||
<ButtonsRow>
|
||||
<IconButton
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
|
@ -145,17 +147,9 @@ function AlignDistribute({
|
|||
>
|
||||
<SpaceEvenlyVerticallyIcon />
|
||||
</IconButton>
|
||||
</Container>
|
||||
</ButtonsRow>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AlignDistribute)
|
||||
|
||||
const Container = styled('div', {
|
||||
display: 'grid',
|
||||
padding: 4,
|
||||
gridTemplateColumns: 'repeat(5, auto)',
|
||||
[`& ${IconButton} > svg`]: {
|
||||
stroke: 'transparent',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
DashDashedIcon,
|
||||
DashDottedIcon,
|
||||
DashSolidIcon,
|
||||
DashDrawIcon,
|
||||
} from '../shared'
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import { DashStyle } from 'types'
|
||||
|
@ -15,6 +16,7 @@ function handleChange(dash: string) {
|
|||
}
|
||||
|
||||
const dashes = {
|
||||
[DashStyle.Draw]: <DashDrawIcon />,
|
||||
[DashStyle.Solid]: <DashSolidIcon />,
|
||||
[DashStyle.Dashed]: <DashDashedIcon />,
|
||||
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||
|
|
|
@ -2,9 +2,9 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
|||
import { breakpoints, IconButton } from 'components/shared'
|
||||
import Tooltip from 'components/tooltip'
|
||||
import { strokes } from 'state/shape-styles'
|
||||
import { Square } from 'react-feather'
|
||||
import { useSelector } from 'state'
|
||||
import ColorContent from './color-content'
|
||||
import { BoxIcon } from '../shared'
|
||||
|
||||
export default function QuickColorSelect(): JSX.Element {
|
||||
const color = useSelector((s) => s.values.selectedStyle.color)
|
||||
|
@ -13,7 +13,7 @@ export default function QuickColorSelect(): JSX.Element {
|
|||
<DropdownMenu.Root dir="ltr">
|
||||
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
|
||||
<Tooltip label="Color">
|
||||
<Square fill={strokes[color]} stroke={strokes[color]} />
|
||||
<BoxIcon fill={strokes[color]} stroke={strokes[color]} />
|
||||
</Tooltip>
|
||||
</DropdownMenu.Trigger>
|
||||
<ColorContent />
|
||||
|
|
|
@ -7,12 +7,14 @@ import { DashStyle } from 'types'
|
|||
import {
|
||||
DropdownContent,
|
||||
Item,
|
||||
DashDrawIcon,
|
||||
DashDottedIcon,
|
||||
DashSolidIcon,
|
||||
DashDashedIcon,
|
||||
} from '../shared'
|
||||
|
||||
const dashes = {
|
||||
[DashStyle.Draw]: <DashDrawIcon />,
|
||||
[DashStyle.Solid]: <DashSolidIcon />,
|
||||
[DashStyle.Dashed]: <DashDashedIcon />,
|
||||
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||
|
|
43
components/style-panel/quick-fill-select.tsx
Normal file
43
components/style-panel/quick-fill-select.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||
import tld from 'utils/tld'
|
||||
import {
|
||||
breakpoints,
|
||||
BoxIcon,
|
||||
IsFilledFillIcon,
|
||||
IconButton,
|
||||
IconWrapper,
|
||||
} from '../shared'
|
||||
import state, { useSelector } from 'state'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
|
||||
function handleIsFilledChange(isFilled: boolean) {
|
||||
state.send('CHANGED_STYLE', { isFilled })
|
||||
}
|
||||
|
||||
export default function IsFilledPicker(): JSX.Element {
|
||||
const isFilled = useSelector((s) => s.values.selectedStyle.isFilled)
|
||||
const canFill = useSelector((s) => {
|
||||
const selectedShapes = tld.getSelectedShapes(s.data)
|
||||
|
||||
return (
|
||||
selectedShapes.length === 0 ||
|
||||
selectedShapes.every((shape) => getShapeUtils(shape).canStyleFill)
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<Checkbox.Root
|
||||
dir="ltr"
|
||||
as={IconButton}
|
||||
bp={breakpoints}
|
||||
checked={isFilled}
|
||||
disabled={!canFill}
|
||||
onCheckedChange={handleIsFilledChange}
|
||||
>
|
||||
<IconWrapper>
|
||||
<BoxIcon />
|
||||
<Checkbox.Indicator as={IsFilledFillIcon} />
|
||||
</IconWrapper>
|
||||
</Checkbox.Root>
|
||||
)
|
||||
}
|
|
@ -1,19 +1,16 @@
|
|||
import tld from 'utils/tld'
|
||||
import state, { useSelector } from 'state'
|
||||
import { IconButton, breakpoints } from 'components/shared'
|
||||
import { IconButton, ButtonsRow, breakpoints } from 'components/shared'
|
||||
import { memo } from 'react'
|
||||
import styled from 'styles'
|
||||
import { MoveType } from 'types'
|
||||
import { MoveType, ShapeType } from 'types'
|
||||
import { Trash2 } from 'react-feather'
|
||||
import Tooltip from 'components/tooltip'
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
AspectRatioIcon,
|
||||
BoxIcon,
|
||||
CopyIcon,
|
||||
EyeClosedIcon,
|
||||
EyeOpenIcon,
|
||||
GroupIcon,
|
||||
LockClosedIcon,
|
||||
LockOpen1Icon,
|
||||
PinBottomIcon,
|
||||
|
@ -29,8 +26,12 @@ function handleDuplicate() {
|
|||
state.send('DUPLICATED')
|
||||
}
|
||||
|
||||
function handleHide() {
|
||||
state.send('TOGGLED_SHAPE_HIDE')
|
||||
function handleGroup() {
|
||||
state.send('GROUPED')
|
||||
}
|
||||
|
||||
function handleUngroup() {
|
||||
state.send('UNGROUPED')
|
||||
}
|
||||
|
||||
function handleLock() {
|
||||
|
@ -74,15 +75,24 @@ function ShapesFunctions() {
|
|||
)
|
||||
})
|
||||
|
||||
const isAllHidden = useSelector((s) => {
|
||||
const page = tld.getPage(s.data)
|
||||
return s.values.selectedIds.every((id) => page.shapes[id].isHidden)
|
||||
const isAllGrouped = useSelector((s) => {
|
||||
const selectedShapes = tld.getSelectedShapes(s.data)
|
||||
return selectedShapes.every(
|
||||
(shape) =>
|
||||
shape.type === ShapeType.Group ||
|
||||
(shape.parentId === selectedShapes[0].parentId &&
|
||||
selectedShapes[0].parentId !== s.data.currentPageId)
|
||||
)
|
||||
})
|
||||
|
||||
const hasSelection = useSelector((s) => {
|
||||
return tld.getSelectedIds(s.data).size > 0
|
||||
})
|
||||
|
||||
const hasMultipleSelection = useSelector((s) => {
|
||||
return tld.getSelectedIds(s.data).size > 1
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonsRow>
|
||||
|
@ -107,17 +117,6 @@ function ShapesFunctions() {
|
|||
</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}
|
||||
|
@ -125,7 +124,7 @@ function ShapesFunctions() {
|
|||
onClick={handleLock}
|
||||
>
|
||||
<Tooltip label="Toogle Locked">
|
||||
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
||||
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon opacity={0.4} />}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
|
@ -136,7 +135,18 @@ function ShapesFunctions() {
|
|||
onClick={handleAspectLock}
|
||||
>
|
||||
<Tooltip label="Toogle Aspect Ratio Lock">
|
||||
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
|
||||
<AspectRatioIcon opacity={isAllAspectLocked ? 1 : 0.4} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
bp={breakpoints}
|
||||
disabled={!isAllGrouped && !hasMultipleSelection}
|
||||
size="small"
|
||||
onClick={isAllGrouped ? handleUngroup : handleGroup}
|
||||
>
|
||||
<Tooltip label="Group">
|
||||
<GroupIcon opacity={isAllGrouped ? 1 : 0.4} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ButtonsRow>
|
||||
|
@ -201,16 +211,3 @@ function ShapesFunctions() {
|
|||
}
|
||||
|
||||
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,
|
||||
})
|
||||
|
|
|
@ -2,18 +2,16 @@ import styled from 'styles'
|
|||
import state, { useSelector } from 'state'
|
||||
import * as Panel from 'components/panel'
|
||||
import { useRef } from 'react'
|
||||
import { IconButton } from 'components/shared'
|
||||
import { IconButton, ButtonsRow } from 'components/shared'
|
||||
import { ChevronDown, X } from 'react-feather'
|
||||
import ShapesFunctions from './shapes-functions'
|
||||
import AlignDistribute from './align-distribute'
|
||||
import SizePicker from './size-picker'
|
||||
import DashPicker from './dash-picker'
|
||||
import QuickColorSelect from './quick-color-select'
|
||||
import ColorPicker from './color-picker'
|
||||
import IsFilledPicker from './is-filled-picker'
|
||||
import QuickSizeSelect from './quick-size-select'
|
||||
import QuickdashSelect from './quick-dash-select'
|
||||
import QuickDashSelect from './quick-dash-select'
|
||||
import QuickFillSelect from './quick-fill-select'
|
||||
import Tooltip from 'components/tooltip'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
const breakpoints = { '@initial': 'mobile', '@sm': 'small' } as any
|
||||
|
||||
|
@ -26,116 +24,70 @@ export default function StylePanel(): JSX.Element {
|
|||
|
||||
return (
|
||||
<StylePanelRoot dir="ltr" ref={rContainer} isOpen={isOpen}>
|
||||
{isOpen ? (
|
||||
<SelectedShapeStyles />
|
||||
) : (
|
||||
<>
|
||||
<ButtonsRow>
|
||||
<QuickColorSelect />
|
||||
<QuickSizeSelect />
|
||||
<QuickdashSelect />
|
||||
<QuickDashSelect />
|
||||
<QuickFillSelect />
|
||||
<IconButton
|
||||
bp={breakpoints}
|
||||
title="Style"
|
||||
size="small"
|
||||
onClick={handleStylePanelOpen}
|
||||
>
|
||||
<Tooltip label="More">
|
||||
<ChevronDown />
|
||||
</Tooltip>
|
||||
<Tooltip label="More">{isOpen ? <X /> : <ChevronDown />}</Tooltip>
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</ButtonsRow>
|
||||
{isOpen && <SelectedShapeContent />}
|
||||
</StylePanelRoot>
|
||||
)
|
||||
}
|
||||
|
||||
// This panel is going to be hard to keep cool, as we're selecting computed
|
||||
// information, based on the user's current selection. We might have to keep
|
||||
// track of this data manually within our state.
|
||||
|
||||
function SelectedShapeStyles(): JSX.Element {
|
||||
function SelectedShapeContent(): JSX.Element {
|
||||
const selectedShapesCount = useSelector((s) => s.values.selectedIds.length)
|
||||
|
||||
return (
|
||||
<Panel.Layout>
|
||||
<Panel.Header side="right">
|
||||
<h3>Style</h3>
|
||||
<IconButton
|
||||
bp={breakpoints}
|
||||
size="small"
|
||||
onClick={handleStylePanelOpen}
|
||||
>
|
||||
<X />
|
||||
</IconButton>
|
||||
</Panel.Header>
|
||||
<Content>
|
||||
<ColorPicker />
|
||||
<IsFilledPicker />
|
||||
<Row>
|
||||
<label htmlFor="size">Size</label>
|
||||
<SizePicker />
|
||||
</Row>
|
||||
<Row>
|
||||
<label htmlFor="dash">Dash</label>
|
||||
<DashPicker />
|
||||
</Row>
|
||||
<>
|
||||
<hr />
|
||||
<ShapesFunctions />
|
||||
<AlignDistribute
|
||||
hasTwoOrMore={selectedShapesCount > 1}
|
||||
hasThreeOrMore={selectedShapesCount > 2}
|
||||
/>
|
||||
</Content>
|
||||
</Panel.Layout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const StylePanelRoot = styled(Panel.Root, {
|
||||
const StylePanelRoot = styled(motion(Panel.Root), {
|
||||
minWidth: 1,
|
||||
width: 184,
|
||||
maxWidth: 184,
|
||||
width: 'fit-content',
|
||||
maxWidth: 'fit-content',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'all',
|
||||
padding: 2,
|
||||
|
||||
'& hr': {
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
marginLeft: '-2px',
|
||||
border: 'none',
|
||||
height: 1,
|
||||
backgroundColor: '$brushFill',
|
||||
width: 'calc(100% + 4px)',
|
||||
},
|
||||
|
||||
variants: {
|
||||
isOpen: {
|
||||
true: {},
|
||||
false: {
|
||||
padding: 2,
|
||||
width: 'fit-content',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Content = styled(Panel.Content, {
|
||||
padding: 8,
|
||||
})
|
||||
|
||||
const Row = styled('div', {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '4px 2px 4px 12px',
|
||||
|
||||
'& label': {
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
fontSize: '$1',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
},
|
||||
|
||||
'& > svg': {
|
||||
position: 'relative',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -89,7 +89,10 @@ class Clipboard {
|
|||
|
||||
// Take a snapshot of the element
|
||||
const s = new XMLSerializer()
|
||||
const svgString = s.serializeToString(svg)
|
||||
const svgString = s
|
||||
.serializeToString(svg)
|
||||
.replaceAll(' ', '')
|
||||
.replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1')
|
||||
|
||||
// Copy to clipboard!
|
||||
try {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data, GroupShape, ShapeType } from 'types'
|
||||
import { deepClone, getCommonBounds } from 'utils'
|
||||
import { deepClone, getCommonBounds, uniqueId } from 'utils'
|
||||
import tld from 'utils/tld'
|
||||
import { createShape, getShapeUtils } from 'state/shape-utils'
|
||||
import commands from '.'
|
||||
|
@ -23,6 +23,7 @@ export default function groupCommand(data: Data): void {
|
|||
// Do we need to ungroup the selected shapes shapes, rather than group them?
|
||||
if (isAllSameParent && initialShapes[0]?.parentId !== currentPageId) {
|
||||
const parent = tld.getShape(data, initialShapes[0]?.parentId) as GroupShape
|
||||
|
||||
if (parent.children.length === initialShapes.length) {
|
||||
commands.ungroup(data)
|
||||
return
|
||||
|
@ -62,6 +63,7 @@ export default function groupCommand(data: Data): void {
|
|||
}
|
||||
|
||||
const newGroupShape = createShape(ShapeType.Group, {
|
||||
id: uniqueId(),
|
||||
parentId: newGroupParentId,
|
||||
point: [commonBounds.minX, commonBounds.minY],
|
||||
size: [commonBounds.width, commonBounds.height],
|
||||
|
|
|
@ -37,12 +37,6 @@ const strokeWidths = {
|
|||
[SizeStyle.Large]: 8,
|
||||
}
|
||||
|
||||
const dashArrays = {
|
||||
[DashStyle.Solid]: () => [1],
|
||||
[DashStyle.Dashed]: (sw: number) => [sw * 2, sw * 4],
|
||||
[DashStyle.Dotted]: (sw: number) => [0, sw * 3],
|
||||
}
|
||||
|
||||
const fontSizes = {
|
||||
[SizeStyle.Small]: 24,
|
||||
[SizeStyle.Medium]: 48,
|
||||
|
@ -54,13 +48,6 @@ export function getStrokeWidth(size: SizeStyle): number {
|
|||
return strokeWidths[size]
|
||||
}
|
||||
|
||||
export function getStrokeDashArray(
|
||||
dash: DashStyle,
|
||||
strokeWidth: number
|
||||
): number[] {
|
||||
return dashArrays[dash](strokeWidth)
|
||||
}
|
||||
|
||||
export function getFontSize(size: SizeStyle): number {
|
||||
return fontSizes[size]
|
||||
}
|
||||
|
|
|
@ -127,27 +127,22 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
|
||||
if (isStraightLine) {
|
||||
const straight_sw =
|
||||
strokeWidth * (style.dash === DashStyle.Solid && bend === 0 ? 1 : 1.618)
|
||||
strokeWidth *
|
||||
(style.dash === DashStyle.Draw && bend === 0 ? 0.9 : 1.618)
|
||||
|
||||
if (shape.style.dash === DashStyle.Solid && !pathCache.has(shape)) {
|
||||
if (shape.style.dash === DashStyle.Draw && !pathCache.has(shape)) {
|
||||
renderFreehandArrowShaft(shape)
|
||||
}
|
||||
|
||||
const path =
|
||||
shape.style.dash === DashStyle.Solid
|
||||
shape.style.dash === DashStyle.Draw
|
||||
? pathCache.get(shape)
|
||||
: 'M' + start.point + 'L' + end.point
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } =
|
||||
shape.style.dash === DashStyle.Solid
|
||||
? {
|
||||
strokeDasharray: 'none',
|
||||
strokeDashoffset: '0',
|
||||
}
|
||||
: getPerfectDashProps(
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
arrowDist,
|
||||
sw,
|
||||
shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed',
|
||||
shape.style.dash,
|
||||
2
|
||||
)
|
||||
|
||||
|
@ -182,13 +177,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
|
||||
const path = getArrowArcPath(start, end, circle, bend)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } =
|
||||
shape.style.dash === DashStyle.Solid
|
||||
? {
|
||||
strokeDasharray: 'none',
|
||||
strokeDashoffset: '0',
|
||||
}
|
||||
: getPerfectDashProps(
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
getArcLength(
|
||||
[circle[0], circle[1]],
|
||||
circle[2],
|
||||
|
@ -196,7 +185,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
end.point
|
||||
) - 1,
|
||||
sw,
|
||||
shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed',
|
||||
shape.style.dash,
|
||||
2
|
||||
)
|
||||
|
||||
|
@ -520,26 +509,23 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
|
|||
|
||||
const strokeWidth = +getShapeStyle(style).strokeWidth * 2
|
||||
|
||||
const m = vec.add(
|
||||
vec.lrp(start.point, end.point, 0.25 + Math.abs(getRandom()) / 2),
|
||||
[getRandom() * strokeWidth, getRandom() * strokeWidth]
|
||||
)
|
||||
const st = Math.abs(getRandom())
|
||||
|
||||
const stroke = getStroke(
|
||||
[
|
||||
...vec.pointsBetween(start.point, m),
|
||||
...vec.pointsBetween(m, end.point),
|
||||
start.point,
|
||||
...vec.pointsBetween(start.point, end.point),
|
||||
end.point,
|
||||
end.point,
|
||||
end.point,
|
||||
],
|
||||
{
|
||||
size: strokeWidth * 0.82,
|
||||
thinning: 0.6,
|
||||
easing: (t) => t * t * t * t,
|
||||
end: { taper: 4 + getRandom() * 4 },
|
||||
start: { taper: 4 + getRandom() * 4 },
|
||||
simulatePressure: false,
|
||||
size: strokeWidth / 2,
|
||||
thinning: 0.5 + getRandom() * 0.3,
|
||||
easing: (t) => t * t,
|
||||
end: { taper: 1 },
|
||||
start: { taper: 1 + 32 * (st * st * st) },
|
||||
simulatePressure: true,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -570,10 +556,13 @@ function getArrowHeadPoints(shape: ArrowShape, point: number[], angle = 0) {
|
|||
const getRandom = rng(shape.id)
|
||||
|
||||
return {
|
||||
left: vec.add(point, vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())),
|
||||
left: vec.add(
|
||||
point,
|
||||
vec.rot(v, Math.PI / 6 + (Math.PI / 12) * getRandom())
|
||||
),
|
||||
right: vec.add(
|
||||
point,
|
||||
vec.rot(v, -(Math.PI / 6) + (Math.PI / 8) * getRandom())
|
||||
vec.rot(v, -(Math.PI / 6) + (Math.PI / 12) * getRandom())
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
|||
const rx = Math.max(0, radiusX - strokeWidth / 2)
|
||||
const ry = Math.max(0, radiusY - strokeWidth / 2)
|
||||
|
||||
if (style.dash === DashStyle.Solid) {
|
||||
if (style.dash === DashStyle.Draw) {
|
||||
if (!pathCache.has(shape)) {
|
||||
renderPath(shape)
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
|||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
perimeter,
|
||||
strokeWidth * 1.618,
|
||||
shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed',
|
||||
shape.style.dash,
|
||||
4
|
||||
)
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
|||
const styles = getShapeStyle(style)
|
||||
const strokeWidth = +styles.strokeWidth
|
||||
|
||||
if (style.dash === DashStyle.Solid) {
|
||||
if (style.dash === DashStyle.Draw) {
|
||||
if (!pathCache.has(shape.size)) {
|
||||
renderPath(shape)
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
|||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
length,
|
||||
sw,
|
||||
shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed'
|
||||
shape.style.dash
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -751,7 +751,9 @@ const state = createState({
|
|||
{
|
||||
if: 'isToolLocked',
|
||||
to: 'dot.creating',
|
||||
else: { to: 'selecting' },
|
||||
else: {
|
||||
to: 'selecting',
|
||||
},
|
||||
},
|
||||
],
|
||||
CANCELLED: {
|
||||
|
@ -1152,20 +1154,6 @@ const state = createState({
|
|||
},
|
||||
actions: {
|
||||
// Networked Room
|
||||
setRtStatus(data, payload: { id: string; status: string }) {
|
||||
const { status } = payload
|
||||
|
||||
if (!data.room) {
|
||||
data.room = {
|
||||
id: null,
|
||||
status: '',
|
||||
peers: {},
|
||||
}
|
||||
}
|
||||
|
||||
data.room.peers = {}
|
||||
data.room.status = status
|
||||
},
|
||||
addRtShape(data, payload: { pageId: string; shape: Shape }) {
|
||||
const { pageId, shape } = payload
|
||||
// What if the page is in storage?
|
||||
|
@ -1264,6 +1252,7 @@ const state = createState({
|
|||
})
|
||||
|
||||
const siblings = tld.getChildren(data, shape.parentId)
|
||||
|
||||
const childIndex = siblings.length
|
||||
? siblings[siblings.length - 1].childIndex + 1
|
||||
: 1
|
||||
|
|
1
types.ts
1
types.ts
|
@ -108,6 +108,7 @@ export enum SizeStyle {
|
|||
}
|
||||
|
||||
export enum DashStyle {
|
||||
Draw = 'Draw',
|
||||
Solid = 'Solid',
|
||||
Dashed = 'Dashed',
|
||||
Dotted = 'Dotted',
|
||||
|
|
|
@ -317,6 +317,7 @@ export default class StateUtils {
|
|||
|
||||
static getTopParentId(data: Data, id: string): string {
|
||||
const shape = this.getPage(data).shapes[id]
|
||||
|
||||
return shape.parentId === data.currentPageId ||
|
||||
shape.parentId === data.currentParentId
|
||||
? id
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react'
|
||||
import { Bounds, Edge, Corner, BezierCurveSegment } from 'types'
|
||||
import { Bounds, Edge, Corner, BezierCurveSegment, DashStyle } from 'types'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import vec from './vec'
|
||||
import _isMobile from 'ismobilejs'
|
||||
|
@ -421,7 +421,7 @@ export function getArcLength(
|
|||
export function getPerfectDashProps(
|
||||
length: number,
|
||||
strokeWidth: number,
|
||||
style: 'dashed' | 'dotted' = 'dashed',
|
||||
style: DashStyle,
|
||||
snap = 1
|
||||
): {
|
||||
strokeDasharray: string
|
||||
|
@ -431,7 +431,12 @@ export function getPerfectDashProps(
|
|||
let strokeDashoffset: string
|
||||
let ratio: number
|
||||
|
||||
if (style === 'dashed') {
|
||||
if (style === DashStyle.Solid || style === DashStyle.Draw) {
|
||||
return {
|
||||
strokeDasharray: 'none',
|
||||
strokeDashoffset: 'none',
|
||||
}
|
||||
} else if (style === DashStyle.Dashed) {
|
||||
dashLength = strokeWidth * 2
|
||||
ratio = 1
|
||||
strokeDashoffset = (dashLength / 2).toString()
|
||||
|
@ -1726,7 +1731,7 @@ export function getSvgPathFromStroke(stroke: number[][]): string {
|
|||
)
|
||||
|
||||
d.push('Z')
|
||||
return d.join(' ')
|
||||
return d.join(' ').replaceAll(/(\s[0-9]*\.[0-9]{2})([0-9]*)\b/g, '$1')
|
||||
}
|
||||
|
||||
export function debounce<T extends (...args: unknown[]) => unknown>(
|
||||
|
|
Loading…
Reference in a new issue