You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

style-panel.tsx 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import styled from 'styles'
  2. import state, { useSelector } from 'state'
  3. import * as Panel from 'components/panel'
  4. import { useRef } from 'react'
  5. import { IconButton } from 'components/shared'
  6. import * as Checkbox from '@radix-ui/react-checkbox'
  7. import { ChevronDown, Square, Tool, Trash2, X } from 'react-feather'
  8. import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
  9. import { strokes } from 'lib/shape-styles'
  10. import AlignDistribute from './align-distribute'
  11. import { MoveType } from 'types'
  12. import SizePicker from './size-picker'
  13. import {
  14. ArrowDownIcon,
  15. ArrowUpIcon,
  16. AspectRatioIcon,
  17. BoxIcon,
  18. CheckIcon,
  19. CopyIcon,
  20. DotsVerticalIcon,
  21. EyeClosedIcon,
  22. EyeOpenIcon,
  23. LockClosedIcon,
  24. LockOpen1Icon,
  25. PinBottomIcon,
  26. PinTopIcon,
  27. RotateCounterClockwiseIcon,
  28. } from '@radix-ui/react-icons'
  29. import DashPicker from './dash-picker'
  30. import QuickColorSelect from './quick-color-select'
  31. import ColorContent from './color-content'
  32. import { RowButton, IconWrapper } from './shared'
  33. import ColorPicker from './color-picker'
  34. import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
  35. import IsFilledPicker from './is-filled-picker'
  36. import QuickSizeSelect from './quick-size-select'
  37. import QuickdashSelect from './quick-dash-select'
  38. import Tooltip from 'components/tooltip'
  39. export default function StylePanel() {
  40. const rContainer = useRef<HTMLDivElement>(null)
  41. const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
  42. return (
  43. <StylePanelRoot ref={rContainer} isOpen={isOpen}>
  44. {isOpen ? (
  45. <SelectedShapeStyles />
  46. ) : (
  47. <>
  48. <QuickColorSelect />
  49. <QuickSizeSelect />
  50. <QuickdashSelect />
  51. <IconButton
  52. title="Style"
  53. size="small"
  54. onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
  55. >
  56. <Tooltip label="More">
  57. <ChevronDown />
  58. </Tooltip>
  59. </IconButton>
  60. </>
  61. )}
  62. </StylePanelRoot>
  63. )
  64. }
  65. // This panel is going to be hard to keep cool, as we're selecting computed
  66. // information, based on the user's current selection. We might have to keep
  67. // track of this data manually within our state.
  68. function SelectedShapeStyles() {
  69. const selectedIds = useSelector(
  70. (s) => Array.from(s.data.selectedIds.values()),
  71. deepCompareArrays
  72. )
  73. const isAllLocked = useSelector((s) => {
  74. const page = getPage(s.data)
  75. return selectedIds.every((id) => page.shapes[id].isLocked)
  76. })
  77. const isAllAspectLocked = useSelector((s) => {
  78. const page = getPage(s.data)
  79. return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
  80. })
  81. const isAllHidden = useSelector((s) => {
  82. const page = getPage(s.data)
  83. return selectedIds.every((id) => page.shapes[id].isHidden)
  84. })
  85. const commonStyle = useSelector((s) => s.values.selectedStyle, deepCompare)
  86. const hasSelection = selectedIds.length > 0
  87. return (
  88. <Panel.Layout>
  89. <Panel.Header side="right">
  90. <h3>Style</h3>
  91. <IconButton
  92. size="small"
  93. onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
  94. >
  95. <X />
  96. </IconButton>
  97. </Panel.Header>
  98. <Content>
  99. <ColorPicker
  100. color={commonStyle.color}
  101. onChange={(color) => state.send('CHANGED_STYLE', { color })}
  102. />
  103. <IsFilledPicker
  104. isFilled={commonStyle.isFilled}
  105. onChange={(isFilled) => state.send('CHANGED_STYLE', { isFilled })}
  106. />
  107. <Row>
  108. <label htmlFor="size">Size</label>
  109. <SizePicker size={commonStyle.size} />
  110. </Row>
  111. <Row>
  112. <label htmlFor="dash">Dash</label>
  113. <DashPicker dash={commonStyle.dash} />
  114. </Row>
  115. <ButtonsRow>
  116. <IconButton
  117. disabled={!hasSelection}
  118. size="small"
  119. onClick={() => state.send('DUPLICATED')}
  120. >
  121. <Tooltip label="Duplicate">
  122. <CopyIcon />
  123. </Tooltip>
  124. </IconButton>
  125. <IconButton
  126. disabled={!hasSelection}
  127. size="small"
  128. onClick={() => state.send('ROTATED_CCW')}
  129. >
  130. <Tooltip label="Rotate">
  131. <RotateCounterClockwiseIcon />
  132. </Tooltip>
  133. </IconButton>
  134. <IconButton
  135. disabled={!hasSelection}
  136. size="small"
  137. onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
  138. >
  139. <Tooltip label="Toogle Hidden">
  140. {isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
  141. </Tooltip>
  142. </IconButton>
  143. <IconButton
  144. disabled={!hasSelection}
  145. size="small"
  146. onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
  147. >
  148. <Tooltip label="Toogle Locked">
  149. {isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
  150. </Tooltip>
  151. </IconButton>
  152. <IconButton
  153. disabled={!hasSelection}
  154. size="small"
  155. onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
  156. >
  157. <Tooltip label="Toogle Aspect Ratio Lock">
  158. {isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
  159. </Tooltip>
  160. </IconButton>
  161. </ButtonsRow>
  162. <ButtonsRow>
  163. <IconButton
  164. disabled={!hasSelection}
  165. size="small"
  166. onClick={() => state.send('MOVED', { type: MoveType.ToBack })}
  167. >
  168. <Tooltip label="Move to Back">
  169. <PinBottomIcon />
  170. </Tooltip>
  171. </IconButton>
  172. <IconButton
  173. disabled={!hasSelection}
  174. size="small"
  175. onClick={() => state.send('MOVED', { type: MoveType.Backward })}
  176. >
  177. <Tooltip label="Move Backward">
  178. <ArrowDownIcon />
  179. </Tooltip>
  180. </IconButton>
  181. <IconButton
  182. disabled={!hasSelection}
  183. size="small"
  184. onClick={() => state.send('MOVED', { type: MoveType.Forward })}
  185. >
  186. <Tooltip label="Move Forward">
  187. <ArrowUpIcon />
  188. </Tooltip>
  189. </IconButton>
  190. <IconButton
  191. disabled={!hasSelection}
  192. size="small"
  193. onClick={() => state.send('MOVED', { type: MoveType.ToFront })}
  194. >
  195. <Tooltip label="More to Front">
  196. <PinTopIcon />
  197. </Tooltip>
  198. </IconButton>
  199. <IconButton
  200. disabled={!hasSelection}
  201. size="small"
  202. onClick={() => state.send('DELETED')}
  203. >
  204. <Tooltip label="Delete">
  205. <Trash2 size="15" />
  206. </Tooltip>
  207. </IconButton>
  208. </ButtonsRow>
  209. <AlignDistribute
  210. hasTwoOrMore={selectedIds.length > 1}
  211. hasThreeOrMore={selectedIds.length > 2}
  212. />
  213. </Content>
  214. </Panel.Layout>
  215. )
  216. }
  217. const StylePanelRoot = styled(Panel.Root, {
  218. minWidth: 1,
  219. width: 184,
  220. maxWidth: 184,
  221. overflow: 'hidden',
  222. position: 'relative',
  223. border: '1px solid $panel',
  224. boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
  225. display: 'flex',
  226. alignItems: 'center',
  227. pointerEvents: 'all',
  228. variants: {
  229. isOpen: {
  230. true: {},
  231. false: {
  232. padding: 2,
  233. width: 'fit-content',
  234. },
  235. },
  236. },
  237. })
  238. const Content = styled(Panel.Content, {
  239. padding: 8,
  240. })
  241. const Row = styled('div', {
  242. position: 'relative',
  243. display: 'flex',
  244. width: '100%',
  245. background: 'none',
  246. border: 'none',
  247. outline: 'none',
  248. alignItems: 'center',
  249. justifyContent: 'space-between',
  250. padding: '4px 2px 4px 12px',
  251. '& label': {
  252. fontFamily: '$ui',
  253. fontSize: '$2',
  254. fontWeight: '$1',
  255. margin: 0,
  256. padding: 0,
  257. },
  258. '& > svg': {
  259. position: 'relative',
  260. },
  261. })
  262. const ButtonsRow = styled('div', {
  263. position: 'relative',
  264. display: 'flex',
  265. width: '100%',
  266. background: 'none',
  267. border: 'none',
  268. cursor: 'pointer',
  269. outline: 'none',
  270. alignItems: 'center',
  271. justifyContent: 'flex-start',
  272. padding: 4,
  273. })