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.6KB

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