tldraw/components/style-panel/style-panel.tsx

315 lines
8.2 KiB
TypeScript
Raw Normal View History

2021-05-28 20:30:27 +00:00
import styled from 'styles'
import state, { useSelector } from 'state'
import * as Panel from 'components/panel'
import { useRef } from 'react'
import { IconButton } from 'components/shared'
2021-06-01 21:49:32 +00:00
import * as Checkbox from '@radix-ui/react-checkbox'
import { Trash2, X } from 'react-feather'
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
2021-05-28 20:30:27 +00:00
import { shades, fills, strokes } from 'lib/colors'
2021-06-01 08:56:41 +00:00
import ColorPicker, { ColorIcon, CurrentColor } from './color-picker'
2021-05-28 20:30:27 +00:00
import AlignDistribute from './align-distribute'
2021-05-29 22:27:19 +00:00
import { MoveType, ShapeStyles } from 'types'
2021-05-28 20:30:27 +00:00
import WidthPicker from './width-picker'
import {
2021-05-29 22:27:19 +00:00
ArrowDownIcon,
ArrowUpIcon,
AspectRatioIcon,
BoxIcon,
2021-06-01 21:49:32 +00:00
CheckIcon,
CopyIcon,
2021-06-01 21:49:32 +00:00
DotsVerticalIcon,
EyeClosedIcon,
EyeOpenIcon,
LockClosedIcon,
LockOpen1Icon,
2021-05-29 22:27:19 +00:00
PinBottomIcon,
PinTopIcon,
RotateCounterClockwiseIcon,
} from '@radix-ui/react-icons'
2021-06-01 21:49:32 +00:00
import DashPicker from './dash-picker'
2021-05-26 21:47:46 +00:00
const fillColors = { ...shades, ...fills }
const strokeColors = { ...shades, ...strokes }
2021-06-01 21:49:32 +00:00
const getFillColor = (color: string) => {
if (shades[color]) {
return '#fff'
}
return fillColors[color]
}
2021-05-26 10:34:10 +00:00
export default function StylePanel() {
const rContainer = useRef<HTMLDivElement>(null)
const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
return (
<StylePanelRoot ref={rContainer} isOpen={isOpen}>
{isOpen ? (
<SelectedShapeStyles />
) : (
2021-06-01 21:49:32 +00:00
<>
<QuickColorSelect prop="stroke" colors={strokeColors} />
<IconButton
title="Style"
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
>
<DotsVerticalIcon />
</IconButton>
</>
2021-05-26 10:34:10 +00:00
)}
</StylePanelRoot>
)
}
2021-06-01 21:49:32 +00:00
function QuickColorSelect({
prop,
colors,
}: {
prop: ShapeStyles['fill'] | ShapeStyles['stroke']
colors: Record<string, string>
}) {
const value = useSelector((s) => s.values.selectedStyle[prop])
return (
<ColorPicker
colors={colors}
onChange={(color) => state.send('CHANGED_STYLE', { [prop]: color })}
>
<CurrentColor size="icon" title={prop}>
<ColorIcon color={value} />
</CurrentColor>
</ColorPicker>
)
}
2021-05-26 10:34:10 +00:00
// 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({}: {}) {
const selectedIds = useSelector(
(s) => Array.from(s.data.selectedIds.values()),
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)
})
2021-06-01 21:49:32 +00:00
const commonStyle = useSelector((s) => s.values.selectedStyle, deepCompare)
2021-05-26 10:34:10 +00:00
2021-05-26 21:47:46 +00:00
const hasSelection = selectedIds.length > 0
2021-05-26 10:34:10 +00:00
return (
<Panel.Layout>
2021-05-28 20:30:27 +00:00
<Panel.Header side="right">
2021-05-26 10:34:10 +00:00
<h3>Style</h3>
2021-05-28 20:30:27 +00:00
<IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
2021-05-27 17:59:40 +00:00
<X />
</IconButton>
2021-05-26 10:34:10 +00:00
</Panel.Header>
2021-05-26 19:20:52 +00:00
<Content>
<ColorPicker
2021-05-26 21:47:46 +00:00
colors={strokeColors}
2021-06-01 21:49:32 +00:00
onChange={(color) =>
state.send('CHANGED_STYLE', {
stroke: strokeColors[color],
fill: getFillColor(color),
})
}
2021-06-01 08:56:41 +00:00
>
<CurrentColor>
2021-06-01 21:49:32 +00:00
<label>Color</label>
2021-06-01 08:56:41 +00:00
<ColorIcon color={commonStyle.stroke} />
</CurrentColor>
</ColorPicker>
2021-06-01 21:49:32 +00:00
{/* <Row>
<label htmlFor="filled">Filled</label>
<StyledCheckbox
checked={commonStyle.isFilled}
onCheckedChange={(e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
state.send('CHANGED_STYLE', {
isFilled: e.target.value === 'on',
})
}}
>
<Checkbox.Indicator as={CheckIcon} />
</StyledCheckbox>
</Row>*/}
2021-05-28 20:30:27 +00:00
<Row>
<label htmlFor="width">Width</label>
<WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
2021-05-28 20:30:27 +00:00
</Row>
2021-06-01 21:49:32 +00:00
<Row>
<label htmlFor="dash">Dash</label>
<DashPicker dash={commonStyle.dash} />
</Row>
2021-05-28 20:30:27 +00:00
<ButtonsRow>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('DUPLICATED')}
>
<CopyIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('ROTATED_CCW')}
>
<RotateCounterClockwiseIcon />
</IconButton>
2021-05-28 20:30:27 +00:00
<IconButton
disabled={!hasSelection}
onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
2021-05-28 20:30:27 +00:00
>
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
2021-05-28 20:30:27 +00:00
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
>
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
>
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
2021-05-28 20:30:27 +00:00
</IconButton>
2021-05-29 22:27:19 +00:00
</ButtonsRow>
<ButtonsRow>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('MOVED', { type: MoveType.ToBack })}
>
<PinBottomIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('MOVED', { type: MoveType.Backward })}
>
<ArrowDownIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('MOVED', { type: MoveType.Forward })}
>
<ArrowUpIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('MOVED', { type: MoveType.ToFront })}
>
<PinTopIcon />
</IconButton>
2021-05-29 13:59:11 +00:00
<IconButton
disabled={!hasSelection}
onClick={() => state.send('DELETED')}
>
2021-06-01 08:56:41 +00:00
<Trash2 />
2021-05-29 13:59:11 +00:00
</IconButton>
2021-05-28 20:30:27 +00:00
</ButtonsRow>
2021-05-29 22:27:19 +00:00
<AlignDistribute
hasTwoOrMore={selectedIds.length > 1}
hasThreeOrMore={selectedIds.length > 2}
/>
2021-05-26 19:20:52 +00:00
</Content>
2021-05-26 10:34:10 +00:00
</Panel.Layout>
)
}
const StylePanelRoot = styled(Panel.Root, {
2021-05-26 19:20:52 +00:00
minWidth: 1,
width: 184,
maxWidth: 184,
2021-05-28 20:30:27 +00:00
overflow: 'hidden',
position: 'relative',
2021-05-30 13:49:33 +00:00
border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
2021-06-01 21:49:32 +00:00
display: 'flex',
alignItems: 'center',
pointerEvents: 'all',
2021-05-26 10:34:10 +00:00
variants: {
isOpen: {
2021-05-26 19:20:52 +00:00
true: {},
2021-05-26 10:34:10 +00:00
false: {
2021-05-30 13:49:33 +00:00
padding: 2,
2021-06-01 21:49:32 +00:00
width: 'fit-content',
2021-05-26 10:34:10 +00:00
},
},
},
})
2021-05-26 19:20:52 +00:00
const Content = styled(Panel.Content, {
padding: 8,
2021-05-26 10:34:10 +00:00
})
2021-05-28 20:30:27 +00:00
const Row = styled('div', {
position: 'relative',
display: 'flex',
width: '100%',
background: 'none',
border: 'none',
cursor: 'pointer',
outline: 'none',
alignItems: 'center',
justifyContent: 'space-between',
padding: '4px 2px 4px 12px',
'& label': {
fontFamily: '$ui',
fontSize: '$2',
fontWeight: '$1',
margin: 0,
padding: 0,
},
'& > svg': {
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,
})
2021-06-01 21:49:32 +00:00
const StyledCheckbox = styled(Checkbox.Root, {
appearance: 'none',
backgroundColor: 'transparent',
border: 'none',
padding: 0,
boxShadow: 'inset 0 0 0 1px gainsboro',
width: 15,
height: 15,
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&:focus': {
outline: 'none',
boxShadow: 'inset 0 0 0 1px dodgerblue, 0 0 0 1px dodgerblue',
},
})