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 8.5KB

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