Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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 { Circle, Copy, Lock, Trash, Unlock, X } from 'react-feather'
  7. import {
  8. deepCompare,
  9. deepCompareArrays,
  10. getPage,
  11. getSelectedShapes,
  12. } from 'utils/utils'
  13. import { shades, fills, strokes } from 'lib/colors'
  14. import ColorPicker from './color-picker'
  15. import AlignDistribute from './align-distribute'
  16. import { ShapeStyles } from 'types'
  17. import WidthPicker from './width-picker'
  18. import {
  19. AspectRatioIcon,
  20. BoxIcon,
  21. CopyIcon,
  22. EyeClosedIcon,
  23. EyeOpenIcon,
  24. LockClosedIcon,
  25. LockOpen1Icon,
  26. RotateCounterClockwiseIcon,
  27. TrashIcon,
  28. } from '@radix-ui/react-icons'
  29. const fillColors = { ...shades, ...fills }
  30. const strokeColors = { ...shades, ...strokes }
  31. export default function StylePanel() {
  32. const rContainer = useRef<HTMLDivElement>(null)
  33. const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
  34. return (
  35. <StylePanelRoot ref={rContainer} isOpen={isOpen}>
  36. {isOpen ? (
  37. <SelectedShapeStyles />
  38. ) : (
  39. <IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
  40. <Circle />
  41. </IconButton>
  42. )}
  43. </StylePanelRoot>
  44. )
  45. }
  46. // This panel is going to be hard to keep cool, as we're selecting computed
  47. // information, based on the user's current selection. We might have to keep
  48. // track of this data manually within our state.
  49. function SelectedShapeStyles({}: {}) {
  50. const selectedIds = useSelector(
  51. (s) => Array.from(s.data.selectedIds.values()),
  52. deepCompareArrays
  53. )
  54. const isAllLocked = useSelector((s) => {
  55. const page = getPage(s.data)
  56. return selectedIds.every((id) => page.shapes[id].isLocked)
  57. })
  58. const isAllAspectLocked = useSelector((s) => {
  59. const page = getPage(s.data)
  60. return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
  61. })
  62. const isAllHidden = useSelector((s) => {
  63. const page = getPage(s.data)
  64. return selectedIds.every((id) => page.shapes[id].isHidden)
  65. })
  66. const commonStyle = useSelector((s) => {
  67. const { currentStyle } = s.data
  68. if (selectedIds.length === 0) {
  69. return currentStyle
  70. }
  71. const page = getPage(s.data)
  72. const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
  73. const commonStyle: Partial<ShapeStyles> = {}
  74. const overrides = new Set<string>([])
  75. for (const shapeStyle of shapeStyles) {
  76. for (let key in currentStyle) {
  77. if (overrides.has(key)) continue
  78. if (commonStyle[key] === undefined) {
  79. commonStyle[key] = shapeStyle[key]
  80. } else {
  81. if (commonStyle[key] === shapeStyle[key]) continue
  82. commonStyle[key] = currentStyle[key]
  83. overrides.add(key)
  84. }
  85. }
  86. }
  87. return commonStyle
  88. }, deepCompare)
  89. const hasSelection = selectedIds.length > 0
  90. return (
  91. <Panel.Layout>
  92. <Panel.Header side="right">
  93. <h3>Style</h3>
  94. <IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
  95. <X />
  96. </IconButton>
  97. </Panel.Header>
  98. <Content>
  99. <ColorPicker
  100. label="Fill"
  101. color={commonStyle.fill}
  102. colors={fillColors}
  103. onChange={(color) => state.send('CHANGED_STYLE', { fill: color })}
  104. />
  105. <ColorPicker
  106. label="Stroke"
  107. color={commonStyle.stroke}
  108. colors={strokeColors}
  109. onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })}
  110. />
  111. <Row>
  112. <label htmlFor="width">Width</label>
  113. <WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
  114. </Row>
  115. <AlignDistribute
  116. hasTwoOrMore={selectedIds.length > 1}
  117. hasThreeOrMore={selectedIds.length > 2}
  118. />
  119. <ButtonsRow>
  120. <IconButton
  121. disabled={!hasSelection}
  122. onClick={() => state.send('DUPLICATED')}
  123. >
  124. <CopyIcon />
  125. </IconButton>
  126. <IconButton
  127. disabled={!hasSelection}
  128. onClick={() => state.send('ROTATED_CCW')}
  129. >
  130. <RotateCounterClockwiseIcon />
  131. </IconButton>
  132. <IconButton
  133. disabled={!hasSelection}
  134. onClick={() => state.send('DELETED')}
  135. >
  136. <TrashIcon />
  137. </IconButton>
  138. <IconButton
  139. disabled={!hasSelection}
  140. onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
  141. >
  142. {isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
  143. </IconButton>
  144. <IconButton
  145. disabled={!hasSelection}
  146. onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
  147. >
  148. {isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
  149. </IconButton>
  150. <IconButton
  151. disabled={!hasSelection}
  152. onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
  153. >
  154. {isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
  155. </IconButton>
  156. </ButtonsRow>
  157. </Content>
  158. </Panel.Layout>
  159. )
  160. }
  161. const StylePanelRoot = styled(Panel.Root, {
  162. minWidth: 1,
  163. width: 184,
  164. maxWidth: 184,
  165. overflow: 'hidden',
  166. position: 'relative',
  167. variants: {
  168. isOpen: {
  169. true: {},
  170. false: {
  171. height: 34,
  172. width: 34,
  173. },
  174. },
  175. },
  176. })
  177. const Content = styled(Panel.Content, {
  178. padding: 8,
  179. })
  180. const Row = styled('div', {
  181. position: 'relative',
  182. display: 'flex',
  183. width: '100%',
  184. background: 'none',
  185. border: 'none',
  186. cursor: 'pointer',
  187. outline: 'none',
  188. alignItems: 'center',
  189. justifyContent: 'space-between',
  190. padding: '4px 2px 4px 12px',
  191. '& label': {
  192. fontFamily: '$ui',
  193. fontSize: '$2',
  194. fontWeight: '$1',
  195. margin: 0,
  196. padding: 0,
  197. },
  198. '& > svg': {
  199. position: 'relative',
  200. },
  201. })
  202. const ButtonsRow = styled('div', {
  203. position: 'relative',
  204. display: 'flex',
  205. width: '100%',
  206. background: 'none',
  207. border: 'none',
  208. cursor: 'pointer',
  209. outline: 'none',
  210. alignItems: 'center',
  211. justifyContent: 'flex-start',
  212. padding: 4,
  213. })