Browse Source

[improvement] UI (#215)

* move folders out of packages

* Remove custom yarn stuff, remove duplicate readme

* Remove stitches config

* Add README script.

* bump deps

* Fix script

* Update package.json

* rehauls UI

* further rehauls UI

* UI polish

* Update ToolButton.tsx

* Update ToolButton.tsx

* Bump license

* move tldraw to root

* Remove SW
main
Steve Ruiz 4 years ago
parent
commit
e2369003c6
No account linked to committer's email address
100 changed files with 2404 additions and 1502 deletions
  1. 0
    0
      LICENSE.md
  2. 21
    0
      packages/tldraw/LICENSE.md
  3. BIN
      packages/tldraw/card-repo.png
  4. 2
    2
      packages/tldraw/package.json
  5. 2
    2
      packages/tldraw/scripts/copy-readme.js
  6. 1
    1
      packages/tldraw/src/TLDraw.test.tsx
  7. 74
    63
      packages/tldraw/src/TLDraw.tsx
  8. 11
    0
      packages/tldraw/src/components/ContextMenu/CMIconButton.tsx
  9. 11
    0
      packages/tldraw/src/components/ContextMenu/CMRowButton.tsx
  10. 15
    0
      packages/tldraw/src/components/ContextMenu/CMTriggerButton.tsx
  11. 1
    1
      packages/tldraw/src/components/ContextMenu/ContextMenu.test.tsx
  12. 372
    0
      packages/tldraw/src/components/ContextMenu/ContextMenu.tsx
  13. 1
    0
      packages/tldraw/src/components/ContextMenu/index.ts
  14. 12
    0
      packages/tldraw/src/components/Divider/Divider.tsx
  15. 1
    0
      packages/tldraw/src/components/Divider/index.ts
  16. 5
    0
      packages/tldraw/src/components/DropdownMenu/DMArrow.tsx
  17. 33
    0
      packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx
  18. 36
    0
      packages/tldraw/src/components/DropdownMenu/DMContent.tsx
  19. 11
    0
      packages/tldraw/src/components/DropdownMenu/DMDivider.tsx
  20. 23
    0
      packages/tldraw/src/components/DropdownMenu/DMIconButton.tsx
  21. 11
    0
      packages/tldraw/src/components/DropdownMenu/DMItem.tsx
  22. 26
    0
      packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx
  23. 28
    0
      packages/tldraw/src/components/DropdownMenu/DMSubMenu.tsx
  24. 15
    0
      packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx
  25. 9
    0
      packages/tldraw/src/components/DropdownMenu/index.tsx
  26. 32
    0
      packages/tldraw/src/components/FocusButton/FocusButton.tsx
  27. 0
    0
      packages/tldraw/src/components/FocusButton/index.ts
  28. 5
    9
      packages/tldraw/src/components/IconButton/IconButton.tsx
  29. 1
    0
      packages/tldraw/src/components/IconButton/index.ts
  30. 5
    7
      packages/tldraw/src/components/Kbd/Kbd.tsx
  31. 1
    0
      packages/tldraw/src/components/Kbd/index.ts
  32. 17
    0
      packages/tldraw/src/components/MenuContent/MenuContent.ts
  33. 1
    0
      packages/tldraw/src/components/MenuContent/index.ts
  34. 30
    0
      packages/tldraw/src/components/Panel/Panel.tsx
  35. 1
    0
      packages/tldraw/src/components/Panel/index.ts
  36. 142
    0
      packages/tldraw/src/components/RowButton/RowButton.tsx
  37. 1
    0
      packages/tldraw/src/components/RowButton/index.ts
  38. 27
    0
      packages/tldraw/src/components/SmallIcon/SmallIcon.tsx
  39. 1
    0
      packages/tldraw/src/components/SmallIcon/index.ts
  40. 132
    0
      packages/tldraw/src/components/ToolButton/ToolButton.tsx
  41. 1
    0
      packages/tldraw/src/components/ToolButton/index.ts
  42. 289
    0
      packages/tldraw/src/components/ToolsPanel/ActionButton.tsx
  43. 8
    8
      packages/tldraw/src/components/ToolsPanel/BackToContent.tsx
  44. 22
    0
      packages/tldraw/src/components/ToolsPanel/LockButton.tsx
  45. 35
    22
      packages/tldraw/src/components/ToolsPanel/PrimaryTools.tsx
  46. 9
    8
      packages/tldraw/src/components/ToolsPanel/StatusBar.tsx
  47. 1
    1
      packages/tldraw/src/components/ToolsPanel/ToolsPanel.test.tsx
  48. 78
    0
      packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx
  49. 1
    0
      packages/tldraw/src/components/ToolsPanel/index.ts
  50. 7
    13
      packages/tldraw/src/components/Tooltip/Tooltip.tsx
  51. 1
    0
      packages/tldraw/src/components/Tooltip/index.ts
  52. 43
    0
      packages/tldraw/src/components/TopPanel/ColorMenu.tsx
  53. 100
    0
      packages/tldraw/src/components/TopPanel/DashMenu.tsx
  54. 31
    0
      packages/tldraw/src/components/TopPanel/FillCheckbox.tsx
  55. 111
    0
      packages/tldraw/src/components/TopPanel/Menu.tsx
  56. 39
    36
      packages/tldraw/src/components/TopPanel/PageMenu.tsx
  57. 139
    0
      packages/tldraw/src/components/TopPanel/PageOptionsDialog.tsx
  58. 70
    0
      packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx
  59. 39
    0
      packages/tldraw/src/components/TopPanel/SizeMenu.tsx
  60. 58
    0
      packages/tldraw/src/components/TopPanel/TopPanel.tsx
  61. 43
    0
      packages/tldraw/src/components/TopPanel/ZoomMenu.tsx
  62. 1
    0
      packages/tldraw/src/components/TopPanel/index.ts
  63. 0
    0
      packages/tldraw/src/components/breakpoints.tsx
  64. 0
    381
      packages/tldraw/src/components/context-menu/context-menu.tsx
  65. 0
    1
      packages/tldraw/src/components/context-menu/index.ts
  66. 22
    0
      packages/tldraw/src/components/icons/BoxIcon.tsx
  67. 2
    2
      packages/tldraw/src/components/icons/CheckIcon.tsx
  68. 0
    0
      packages/tldraw/src/components/icons/CircleIcon.tsx
  69. 17
    0
      packages/tldraw/src/components/icons/DashDashedIcon.tsx
  70. 19
    0
      packages/tldraw/src/components/icons/DashDottedIcon.tsx
  71. 19
    0
      packages/tldraw/src/components/icons/DashDrawIcon.tsx
  72. 9
    0
      packages/tldraw/src/components/icons/DashSolidIcon.tsx
  73. 18
    0
      packages/tldraw/src/components/icons/IsFilledIcon.tsx
  74. 2
    4
      packages/tldraw/src/components/icons/RedoIcon.tsx
  75. 12
    0
      packages/tldraw/src/components/icons/SizeLargeIcon.tsx
  76. 12
    0
      packages/tldraw/src/components/icons/SizeMediumIcon.tsx
  77. 12
    0
      packages/tldraw/src/components/icons/SizeSmallIcon.tsx
  78. 1
    3
      packages/tldraw/src/components/icons/TrashIcon.tsx
  79. 2
    4
      packages/tldraw/src/components/icons/UndoIcon.tsx
  80. 14
    0
      packages/tldraw/src/components/icons/index.ts
  81. 0
    5
      packages/tldraw/src/components/icons/index.tsx
  82. 0
    5
      packages/tldraw/src/components/index.ts
  83. 0
    1
      packages/tldraw/src/components/menu/index.ts
  84. 0
    9
      packages/tldraw/src/components/menu/menu.test.tsx
  85. 0
    95
      packages/tldraw/src/components/menu/menu.tsx
  86. 0
    83
      packages/tldraw/src/components/menu/preferences.tsx
  87. 0
    1
      packages/tldraw/src/components/page-options-dialog/index.ts
  88. 0
    9
      packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx
  89. 0
    106
      packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx
  90. 0
    1
      packages/tldraw/src/components/page-panel/index.ts
  91. 0
    9
      packages/tldraw/src/components/page-panel/page-panel.test.tsx
  92. 0
    18
      packages/tldraw/src/components/shared/buttons-row.tsx
  93. 0
    166
      packages/tldraw/src/components/shared/context-menu.tsx
  94. 0
    51
      packages/tldraw/src/components/shared/dialog.tsx
  95. 0
    205
      packages/tldraw/src/components/shared/dropdown-menu.tsx
  96. 0
    44
      packages/tldraw/src/components/shared/floating-container.tsx
  97. 0
    47
      packages/tldraw/src/components/shared/icon-wrapper.tsx
  98. 0
    13
      packages/tldraw/src/components/shared/index.ts
  99. 0
    66
      packages/tldraw/src/components/shared/menu.tsx
  100. 0
    0
      packages/tldraw/src/components/shared/radio-group.tsx

LICENSE → LICENSE.md View File


+ 21
- 0
packages/tldraw/LICENSE.md View File

1
+MIT License
2
+
3
+Copyright (c) 2021 Stephen Ruiz Ltd
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.

BIN
packages/tldraw/card-repo.png View File


+ 2
- 2
packages/tldraw/package.json View File

46
     "@radix-ui/react-id": "^0.1.1",
46
     "@radix-ui/react-id": "^0.1.1",
47
     "@radix-ui/react-radio-group": "^0.1.1",
47
     "@radix-ui/react-radio-group": "^0.1.1",
48
     "@radix-ui/react-tooltip": "^0.1.1",
48
     "@radix-ui/react-tooltip": "^0.1.1",
49
-    "@stitches/core": "^1.2.5",
49
+    "@stitches/react": "^1.2.5",
50
     "@tldraw/core": "^0.1.13",
50
     "@tldraw/core": "^0.1.13",
51
     "@tldraw/intersect": "^0.1.3",
51
     "@tldraw/intersect": "^0.1.3",
52
     "@tldraw/vec": "^0.1.3",
52
     "@tldraw/vec": "^0.1.3",
55
     "rko": "^0.5.25"
55
     "rko": "^0.5.25"
56
   },
56
   },
57
   "gitHead": "083b36e167b6911927a6b58cbbb830b11b33f00a"
57
   "gitHead": "083b36e167b6911927a6b58cbbb830b11b33f00a"
58
-}
58
+}

+ 2
- 2
packages/tldraw/scripts/copy-readme.js View File

1
 /* eslint-disable */
1
 /* eslint-disable */
2
 const fs = require('fs')
2
 const fs = require('fs')
3
 
3
 
4
-const filesToCopy = ['README.md', 'card-repo.png']
4
+const filesToCopy = ['README.md', 'LICENSE.md', 'card-repo.png']
5
 
5
 
6
 filesToCopy.forEach((file) => {
6
 filesToCopy.forEach((file) => {
7
-  fs.copyFile(`../../${file}`, `./dist/${file}`, (err) => {
7
+  fs.copyFile(`../../${file}`, `./${file}`, (err) => {
8
     if (err) throw err
8
     if (err) throw err
9
   })
9
   })
10
 })
10
 })

packages/tldraw/src/components/tldraw/tldraw.test.tsx → packages/tldraw/src/TLDraw.test.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
 import { render } from '@testing-library/react'
2
 import { render } from '@testing-library/react'
3
-import { TLDraw } from './tldraw'
3
+import { TLDraw } from './TLDraw'
4
 
4
 
5
 describe('tldraw', () => {
5
 describe('tldraw', () => {
6
   test('mounts component without crashing', () => {
6
   test('mounts component without crashing', () => {

packages/tldraw/src/components/tldraw/tldraw.tsx → packages/tldraw/src/TLDraw.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
 import { IdProvider } from '@radix-ui/react-id'
2
 import { IdProvider } from '@radix-ui/react-id'
3
 import { Renderer } from '@tldraw/core'
3
 import { Renderer } from '@tldraw/core'
4
-import css, { dark } from '~styles'
4
+import styled, { dark } from '~styles'
5
 import { Data, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types'
5
 import { Data, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types'
6
 import { TLDrawState } from '~state'
6
 import { TLDrawState } from '~state'
7
 import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks'
7
 import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks'
8
 import { shapeUtils } from '~shape-utils'
8
 import { shapeUtils } from '~shape-utils'
9
-import { StylePanel } from '~components/style-panel'
10
-import { ToolsPanel } from '~components/tools-panel'
11
-import { PagePanel } from '~components/page-panel'
12
-import { Menu } from '~components/menu'
13
-import { breakpoints, iconButton } from '~components'
14
-import { DotFilledIcon } from '@radix-ui/react-icons'
9
+import { ToolsPanel } from '~components/ToolsPanel'
10
+import { TopPanel } from '~components/TopPanel'
15
 import { TLDR } from '~state/tldr'
11
 import { TLDR } from '~state/tldr'
16
-import { ContextMenu } from '~components/context-menu'
12
+import { ContextMenu } from '~components/ContextMenu'
13
+import { FocusButton } from '~components/FocusButton/FocusButton'
17
 
14
 
18
 // Selectors
15
 // Selectors
19
 const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select'
16
 const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select'
68
    */
65
    */
69
   showPages?: boolean
66
   showPages?: boolean
70
 
67
 
68
+  /**
69
+   * (optional) Whether to show the styles UI.
70
+   */
71
+  showStyles?: boolean
72
+
73
+  /**
74
+   * (optional) Whether to show the zoom UI.
75
+   */
76
+  showZoom?: boolean
77
+
78
+  /**
79
+   * (optional) Whether to show the tools UI.
80
+   */
81
+  showTools?: boolean
82
+
83
+  /**
84
+   * (optional) Whether to show the UI.
85
+   */
86
+  showUI?: boolean
87
+
71
   /**
88
   /**
72
    * (optional) A callback to run when the component mounts.
89
    * (optional) A callback to run when the component mounts.
73
    */
90
    */
88
   autofocus = true,
105
   autofocus = true,
89
   showMenu = true,
106
   showMenu = true,
90
   showPages = true,
107
   showPages = true,
108
+  showTools = true,
109
+  showZoom = true,
110
+  showStyles = true,
111
+  showUI = true,
91
   onMount,
112
   onMount,
92
   onChange,
113
   onChange,
93
   onUserChange,
114
   onUserChange,
120
           autofocus={autofocus}
141
           autofocus={autofocus}
121
           showPages={showPages}
142
           showPages={showPages}
122
           showMenu={showMenu}
143
           showMenu={showMenu}
144
+          showStyles={showStyles}
145
+          showZoom={showZoom}
146
+          showTools={showTools}
147
+          showUI={showUI}
123
         />
148
         />
124
       </IdProvider>
149
       </IdProvider>
125
     </TLDrawContext.Provider>
150
     </TLDrawContext.Provider>
126
   )
151
   )
127
 }
152
 }
128
 
153
 
154
+interface InnerTLDrawProps {
155
+  id?: string
156
+  currentPageId?: string
157
+  autofocus: boolean
158
+  showPages: boolean
159
+  showMenu: boolean
160
+  showZoom: boolean
161
+  showStyles: boolean
162
+  showUI: boolean
163
+  showTools: boolean
164
+  document?: TLDrawDocument
165
+}
166
+
129
 function InnerTldraw({
167
 function InnerTldraw({
130
   id,
168
   id,
131
   currentPageId,
169
   currentPageId,
132
   autofocus,
170
   autofocus,
133
   showPages,
171
   showPages,
134
   showMenu,
172
   showMenu,
173
+  showZoom,
174
+  showStyles,
175
+  showTools,
176
+  showUI,
135
   document,
177
   document,
136
-}: {
137
-  id?: string
138
-  currentPageId?: string
139
-  autofocus: boolean
140
-  showPages: boolean
141
-  showMenu: boolean
142
-  document?: TLDrawDocument
143
-}) {
178
+}: InnerTLDrawProps) {
144
   const { tlstate, useSelector } = useTLDrawContext()
179
   const { tlstate, useSelector } = useTLDrawContext()
145
 
180
 
146
   const rWrapper = React.useRef<HTMLDivElement>(null)
181
   const rWrapper = React.useRef<HTMLDivElement>(null)
209
   }, [currentPageId, tlstate])
244
   }, [currentPageId, tlstate])
210
 
245
 
211
   return (
246
   return (
212
-    <div
213
-      ref={rWrapper}
214
-      tabIndex={0}
215
-      className={[layout(), settings.isDarkMode ? dark : ''].join(' ')}
216
-    >
247
+    <StyledLayout ref={rWrapper} tabIndex={0} className={settings.isDarkMode ? dark : ''}>
217
       <OneOff focusableRef={rWrapper} autofocus={autofocus} />
248
       <OneOff focusableRef={rWrapper} autofocus={autofocus} />
218
       <ContextMenu>
249
       <ContextMenu>
219
         <Renderer
250
         <Renderer
284
           onKeyUp={tlstate.onKeyUp}
315
           onKeyUp={tlstate.onKeyUp}
285
         />
316
         />
286
       </ContextMenu>
317
       </ContextMenu>
287
-      <div className={ui()}>
288
-        {settings.isFocusMode ? (
289
-          <div className={unfocusButton()}>
290
-            <button className={iconButton({ bp: breakpoints })} onClick={tlstate.toggleFocusMode}>
291
-              <DotFilledIcon />
292
-            </button>
293
-          </div>
294
-        ) : (
295
-          <>
296
-            <div className={menuButtons()}>
297
-              {showMenu && <Menu />}
298
-              {showPages && <PagePanel />}
299
-            </div>
300
-            <div className={spacer()} />
301
-            <StylePanel />
302
-            <ToolsPanel />
303
-          </>
304
-        )}
305
-      </div>
306
-    </div>
318
+      {showUI && (
319
+        <StyledUI>
320
+          {settings.isFocusMode ? (
321
+            <FocusButton onSelect={tlstate.toggleFocusMode} />
322
+          ) : (
323
+            <>
324
+              <TopPanel
325
+                showPages={showPages}
326
+                showMenu={showMenu}
327
+                showZoom={showZoom}
328
+                showStyles={showStyles}
329
+              />
330
+              <StyledSpacer />
331
+              {showTools && <ToolsPanel />}
332
+            </>
333
+          )}
334
+        </StyledUI>
335
+      )}
336
+    </StyledLayout>
307
   )
337
   )
308
 }
338
 }
309
 
339
 
328
   }
358
   }
329
 )
359
 )
330
 
360
 
331
-const layout = css({
361
+const StyledLayout = styled('div', {
332
   position: 'absolute',
362
   position: 'absolute',
333
   height: '100%',
363
   height: '100%',
334
   width: '100%',
364
   width: '100%',
350
   },
380
   },
351
 })
381
 })
352
 
382
 
353
-const ui = css({
383
+const StyledUI = styled('div', {
354
   position: 'absolute',
384
   position: 'absolute',
355
   top: 0,
385
   top: 0,
356
   left: 0,
386
   left: 0,
367
   },
397
   },
368
 })
398
 })
369
 
399
 
370
-const spacer = css({
400
+const StyledSpacer = styled('div', {
371
   flexGrow: 2,
401
   flexGrow: 2,
372
 })
402
 })
373
-
374
-const menuButtons = css({
375
-  display: 'flex',
376
-  gap: 8,
377
-})
378
-
379
-const unfocusButton = css({
380
-  opacity: 1,
381
-  zIndex: 100,
382
-  backgroundColor: 'transparent',
383
-
384
-  '& svg': {
385
-    color: '$muted',
386
-  },
387
-
388
-  '&:hover svg': {
389
-    color: '$text',
390
-  },
391
-})

+ 11
- 0
packages/tldraw/src/components/ContextMenu/CMIconButton.tsx View File

1
+import * as React from 'react'
2
+import { ContextMenuItem } from '@radix-ui/react-context-menu'
3
+import { ToolButton, ToolButtonProps } from '~components/ToolButton'
4
+
5
+export function CMIconButton({ onSelect, ...rest }: ToolButtonProps): JSX.Element {
6
+  return (
7
+    <ContextMenuItem dir="ltr" onSelect={onSelect} asChild>
8
+      <ToolButton {...rest} />
9
+    </ContextMenuItem>
10
+  )
11
+}

+ 11
- 0
packages/tldraw/src/components/ContextMenu/CMRowButton.tsx View File

1
+import * as React from 'react'
2
+import { ContextMenuItem } from '@radix-ui/react-context-menu'
3
+import { RowButton, RowButtonProps } from '~components/RowButton'
4
+
5
+export const CMRowButton = ({ onSelect, ...rest }: RowButtonProps) => {
6
+  return (
7
+    <ContextMenuItem asChild onSelect={onSelect}>
8
+      <RowButton {...rest} />
9
+    </ContextMenuItem>
10
+  )
11
+}

+ 15
- 0
packages/tldraw/src/components/ContextMenu/CMTriggerButton.tsx View File

1
+import * as React from 'react'
2
+import { ContextMenuTriggerItem } from '@radix-ui/react-context-menu'
3
+import { RowButton, RowButtonProps } from '~components/RowButton'
4
+
5
+interface CMTriggerButtonProps extends RowButtonProps {
6
+  isSubmenu?: boolean
7
+}
8
+
9
+export const CMTriggerButton = ({ isSubmenu, ...rest }: CMTriggerButtonProps) => {
10
+  return (
11
+    <ContextMenuTriggerItem asChild>
12
+      <RowButton hasArrow={isSubmenu} {...rest} />
13
+    </ContextMenuTriggerItem>
14
+  )
15
+}

packages/tldraw/src/components/context-menu/context-menu.test.tsx → packages/tldraw/src/components/ContextMenu/ContextMenu.test.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
-import { ContextMenu } from './context-menu'
2
+import { ContextMenu } from './ContextMenu'
3
 import { renderWithContext } from '~test'
3
 import { renderWithContext } from '~test'
4
 
4
 
5
 describe('context menu', () => {
5
 describe('context menu', () => {

+ 372
- 0
packages/tldraw/src/components/ContextMenu/ContextMenu.tsx View File

1
+import * as React from 'react'
2
+import styled from '~styles'
3
+import * as RadixContextMenu from '@radix-ui/react-context-menu'
4
+import { useTLDrawContext } from '~hooks'
5
+import { Data, AlignType, DistributeType, StretchType } from '~types'
6
+import {
7
+  AlignBottomIcon,
8
+  AlignCenterHorizontallyIcon,
9
+  AlignCenterVerticallyIcon,
10
+  AlignLeftIcon,
11
+  AlignRightIcon,
12
+  AlignTopIcon,
13
+  SpaceEvenlyHorizontallyIcon,
14
+  SpaceEvenlyVerticallyIcon,
15
+  StretchHorizontallyIcon,
16
+  StretchVerticallyIcon,
17
+} from '@radix-ui/react-icons'
18
+import { CMRowButton } from './CMRowButton'
19
+import { CMIconButton } from './CMIconButton'
20
+import { CMTriggerButton } from './CMTriggerButton'
21
+import { Divider } from '~components/Divider'
22
+import { MenuContent } from '~components/MenuContent'
23
+
24
+const has1SelectedIdsSelector = (s: Data) => {
25
+  return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
26
+}
27
+const has2SelectedIdsSelector = (s: Data) => {
28
+  return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1
29
+}
30
+const has3SelectedIdsSelector = (s: Data) => {
31
+  return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
32
+}
33
+
34
+const isDebugModeSelector = (s: Data) => {
35
+  return s.settings.isDebugMode
36
+}
37
+
38
+const hasGroupSelectedSelector = (s: Data) => {
39
+  return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
40
+    (id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined
41
+  )
42
+}
43
+
44
+interface ContextMenuProps {
45
+  children: React.ReactNode
46
+}
47
+
48
+export const ContextMenu = ({ children }: ContextMenuProps): JSX.Element => {
49
+  const { tlstate, useSelector } = useTLDrawContext()
50
+  const hasSelection = useSelector(has1SelectedIdsSelector)
51
+  const hasTwoOrMore = useSelector(has2SelectedIdsSelector)
52
+  const hasThreeOrMore = useSelector(has3SelectedIdsSelector)
53
+  const isDebugMode = useSelector(isDebugModeSelector)
54
+  const hasGroupSelected = useSelector(hasGroupSelectedSelector)
55
+
56
+  const rContent = React.useRef<HTMLDivElement>(null)
57
+
58
+  const handleFlipHorizontal = React.useCallback(() => {
59
+    tlstate.flipHorizontal()
60
+  }, [tlstate])
61
+
62
+  const handleFlipVertical = React.useCallback(() => {
63
+    tlstate.flipVertical()
64
+  }, [tlstate])
65
+
66
+  const handleDuplicate = React.useCallback(() => {
67
+    tlstate.duplicate()
68
+  }, [tlstate])
69
+
70
+  const handleGroup = React.useCallback(() => {
71
+    tlstate.group()
72
+  }, [tlstate])
73
+
74
+  const handleMoveToBack = React.useCallback(() => {
75
+    tlstate.moveToBack()
76
+  }, [tlstate])
77
+
78
+  const handleMoveBackward = React.useCallback(() => {
79
+    tlstate.moveBackward()
80
+  }, [tlstate])
81
+
82
+  const handleMoveForward = React.useCallback(() => {
83
+    tlstate.moveForward()
84
+  }, [tlstate])
85
+
86
+  const handleMoveToFront = React.useCallback(() => {
87
+    tlstate.moveToFront()
88
+  }, [tlstate])
89
+
90
+  const handleDelete = React.useCallback(() => {
91
+    tlstate.delete()
92
+  }, [tlstate])
93
+
94
+  const handleCopyJson = React.useCallback(() => {
95
+    tlstate.copyJson()
96
+  }, [tlstate])
97
+
98
+  const handleCopy = React.useCallback(() => {
99
+    tlstate.copy()
100
+  }, [tlstate])
101
+
102
+  const handlePaste = React.useCallback(() => {
103
+    tlstate.paste()
104
+  }, [tlstate])
105
+
106
+  const handleCopySvg = React.useCallback(() => {
107
+    tlstate.copySvg()
108
+  }, [tlstate])
109
+
110
+  const handleUndo = React.useCallback(() => {
111
+    tlstate.undo()
112
+  }, [tlstate])
113
+
114
+  const handleRedo = React.useCallback(() => {
115
+    tlstate.redo()
116
+  }, [tlstate])
117
+
118
+  return (
119
+    <RadixContextMenu.Root>
120
+      <RadixContextMenu.Trigger dir="ltr">{children}</RadixContextMenu.Trigger>
121
+      <RadixContextMenu.Content dir="ltr" ref={rContent} asChild>
122
+        <MenuContent>
123
+          {hasSelection ? (
124
+            <>
125
+              <CMRowButton onSelect={handleFlipHorizontal} kbd="⇧H">
126
+                Flip Horizontal
127
+              </CMRowButton>
128
+              <CMRowButton onSelect={handleFlipVertical} kbd="⇧V">
129
+                Flip Vertical
130
+              </CMRowButton>
131
+              <CMRowButton onSelect={handleDuplicate} kbd="#D">
132
+                Duplicate
133
+              </CMRowButton>
134
+              <Divider />
135
+              {hasTwoOrMore && (
136
+                <CMRowButton onSelect={handleGroup} kbd="#G">
137
+                  Group
138
+                </CMRowButton>
139
+              )}
140
+              <Divider />
141
+              {hasGroupSelected && (
142
+                <CMRowButton onSelect={handleGroup} kbd="#⇧G">
143
+                  Ungroup
144
+                </CMRowButton>
145
+              )}
146
+              <ContextMenuSubMenu label="Move">
147
+                <CMRowButton onSelect={handleMoveToFront} kbd="⇧]">
148
+                  To Front
149
+                </CMRowButton>
150
+                <CMRowButton onSelect={handleMoveForward} kbd="]">
151
+                  Forward
152
+                </CMRowButton>
153
+                <CMRowButton onSelect={handleMoveBackward} kbd="[">
154
+                  Backward
155
+                </CMRowButton>
156
+                <CMRowButton onSelect={handleMoveToBack} kbd="⇧[">
157
+                  To Back
158
+                </CMRowButton>
159
+              </ContextMenuSubMenu>
160
+              <MoveToPageMenu />
161
+              {hasTwoOrMore && (
162
+                <AlignDistributeSubMenu
163
+                  hasTwoOrMore={hasTwoOrMore}
164
+                  hasThreeOrMore={hasThreeOrMore}
165
+                />
166
+              )}
167
+              <Divider />
168
+              <CMRowButton onSelect={handleCopy} kbd="#C">
169
+                Copy
170
+              </CMRowButton>
171
+              <CMRowButton onSelect={handleCopySvg} kbd="⇧#C">
172
+                Copy as SVG
173
+              </CMRowButton>
174
+              {isDebugMode && <CMRowButton onSelect={handleCopyJson}>Copy as JSON</CMRowButton>}
175
+              <CMRowButton onSelect={handlePaste} kbd="#V">
176
+                Paste
177
+              </CMRowButton>
178
+              <Divider />
179
+              <CMRowButton onSelect={handleDelete} kbd="⌫">
180
+                Delete
181
+              </CMRowButton>
182
+            </>
183
+          ) : (
184
+            <>
185
+              <CMRowButton onSelect={handlePaste} kbd="#V">
186
+                Paste
187
+              </CMRowButton>
188
+              <CMRowButton onSelect={handleUndo} kbd="#Z">
189
+                Undo
190
+              </CMRowButton>
191
+              <CMRowButton onSelect={handleRedo} kbd="#⇧Z">
192
+                Redo
193
+              </CMRowButton>
194
+            </>
195
+          )}
196
+        </MenuContent>
197
+      </RadixContextMenu.Content>
198
+    </RadixContextMenu.Root>
199
+  )
200
+}
201
+
202
+function AlignDistributeSubMenu({
203
+  hasThreeOrMore,
204
+}: {
205
+  hasTwoOrMore: boolean
206
+  hasThreeOrMore: boolean
207
+}) {
208
+  const { tlstate } = useTLDrawContext()
209
+
210
+  const alignTop = React.useCallback(() => {
211
+    tlstate.align(AlignType.Top)
212
+  }, [tlstate])
213
+
214
+  const alignCenterVertical = React.useCallback(() => {
215
+    tlstate.align(AlignType.CenterVertical)
216
+  }, [tlstate])
217
+
218
+  const alignBottom = React.useCallback(() => {
219
+    tlstate.align(AlignType.Bottom)
220
+  }, [tlstate])
221
+
222
+  const stretchVertically = React.useCallback(() => {
223
+    tlstate.stretch(StretchType.Vertical)
224
+  }, [tlstate])
225
+
226
+  const distributeVertically = React.useCallback(() => {
227
+    tlstate.distribute(DistributeType.Vertical)
228
+  }, [tlstate])
229
+
230
+  const alignLeft = React.useCallback(() => {
231
+    tlstate.align(AlignType.Left)
232
+  }, [tlstate])
233
+
234
+  const alignCenterHorizontal = React.useCallback(() => {
235
+    tlstate.align(AlignType.CenterHorizontal)
236
+  }, [tlstate])
237
+
238
+  const alignRight = React.useCallback(() => {
239
+    tlstate.align(AlignType.Right)
240
+  }, [tlstate])
241
+
242
+  const stretchHorizontally = React.useCallback(() => {
243
+    tlstate.stretch(StretchType.Horizontal)
244
+  }, [tlstate])
245
+
246
+  const distributeHorizontally = React.useCallback(() => {
247
+    tlstate.distribute(DistributeType.Horizontal)
248
+  }, [tlstate])
249
+
250
+  return (
251
+    <RadixContextMenu.Root>
252
+      <CMTriggerButton isSubmenu>Align / Distribute</CMTriggerButton>
253
+      <RadixContextMenu.Content asChild sideOffset={2} alignOffset={-2}>
254
+        <StyledGridContent selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}>
255
+          <CMIconButton onSelect={alignLeft}>
256
+            <AlignLeftIcon />
257
+          </CMIconButton>
258
+          <CMIconButton onSelect={alignCenterHorizontal}>
259
+            <AlignCenterHorizontallyIcon />
260
+          </CMIconButton>
261
+          <CMIconButton onSelect={alignRight}>
262
+            <AlignRightIcon />
263
+          </CMIconButton>
264
+          <CMIconButton onSelect={stretchHorizontally}>
265
+            <StretchHorizontallyIcon />
266
+          </CMIconButton>
267
+          {hasThreeOrMore && (
268
+            <CMIconButton onSelect={distributeHorizontally}>
269
+              <SpaceEvenlyHorizontallyIcon />
270
+            </CMIconButton>
271
+          )}
272
+          <CMIconButton onSelect={alignTop}>
273
+            <AlignTopIcon />
274
+          </CMIconButton>
275
+          <CMIconButton onSelect={alignCenterVertical}>
276
+            <AlignCenterVerticallyIcon />
277
+          </CMIconButton>
278
+          <CMIconButton onSelect={alignBottom}>
279
+            <AlignBottomIcon />
280
+          </CMIconButton>
281
+          <CMIconButton onSelect={stretchVertically}>
282
+            <StretchVerticallyIcon />
283
+          </CMIconButton>
284
+          {hasThreeOrMore && (
285
+            <CMIconButton onSelect={distributeVertically}>
286
+              <SpaceEvenlyVerticallyIcon />
287
+            </CMIconButton>
288
+          )}
289
+          <CMArrow offset={13} />
290
+        </StyledGridContent>
291
+      </RadixContextMenu.Content>
292
+    </RadixContextMenu.Root>
293
+  )
294
+}
295
+
296
+const StyledGridContent = styled(MenuContent, {
297
+  display: 'grid',
298
+  variants: {
299
+    selectedStyle: {
300
+      threeOrMore: {
301
+        gridTemplateColumns: 'repeat(5, auto)',
302
+      },
303
+      twoOrMore: {
304
+        gridTemplateColumns: 'repeat(4, auto)',
305
+      },
306
+    },
307
+  },
308
+})
309
+
310
+/* ------------------ Move to Page ------------------ */
311
+
312
+const currentPageIdSelector = (s: Data) => s.appState.currentPageId
313
+const documentPagesSelector = (s: Data) => s.document.pages
314
+
315
+function MoveToPageMenu(): JSX.Element | null {
316
+  const { tlstate, useSelector } = useTLDrawContext()
317
+  const currentPageId = useSelector(currentPageIdSelector)
318
+  const documentPages = useSelector(documentPagesSelector)
319
+
320
+  const sorted = Object.values(documentPages)
321
+    .sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
322
+    .filter((a) => a.id !== currentPageId)
323
+
324
+  if (sorted.length === 0) return null
325
+
326
+  return (
327
+    <RadixContextMenu.Root dir="ltr">
328
+      <CMTriggerButton isSubmenu>Move To Page</CMTriggerButton>
329
+      <RadixContextMenu.Content dir="ltr" sideOffset={2} alignOffset={-2} asChild>
330
+        <MenuContent>
331
+          {sorted.map(({ id, name }, i) => (
332
+            <CMRowButton
333
+              key={id}
334
+              disabled={id === currentPageId}
335
+              onSelect={() => tlstate.moveToPage(id)}
336
+            >
337
+              {name || `Page ${i}`}
338
+            </CMRowButton>
339
+          ))}
340
+          <CMArrow offset={13} />
341
+        </MenuContent>
342
+      </RadixContextMenu.Content>
343
+    </RadixContextMenu.Root>
344
+  )
345
+}
346
+
347
+/* --------------------- Submenu -------------------- */
348
+
349
+export interface ContextMenuSubMenuProps {
350
+  label: string
351
+  children: React.ReactNode
352
+}
353
+
354
+export function ContextMenuSubMenu({ children, label }: ContextMenuSubMenuProps): JSX.Element {
355
+  return (
356
+    <RadixContextMenu.Root dir="ltr">
357
+      <CMTriggerButton isSubmenu>{label}</CMTriggerButton>
358
+      <RadixContextMenu.Content dir="ltr" sideOffset={2} alignOffset={-2} asChild>
359
+        <MenuContent>
360
+          {children}
361
+          <CMArrow offset={13} />
362
+        </MenuContent>
363
+      </RadixContextMenu.Content>
364
+    </RadixContextMenu.Root>
365
+  )
366
+}
367
+
368
+/* ---------------------- Arrow --------------------- */
369
+
370
+const CMArrow = styled(RadixContextMenu.ContextMenuArrow, {
371
+  fill: '$panel',
372
+})

+ 1
- 0
packages/tldraw/src/components/ContextMenu/index.ts View File

1
+export * from './ContextMenu'

+ 12
- 0
packages/tldraw/src/components/Divider/Divider.tsx View File

1
+import * as React from 'react'
2
+import styled from '~styles'
3
+
4
+export const Divider = styled('hr', {
5
+  height: 1,
6
+  marginTop: '$1',
7
+  marginRight: '-$2',
8
+  marginBottom: '$1',
9
+  marginLeft: '-$2',
10
+  border: 'none',
11
+  borderBottom: '1px solid $hover',
12
+})

+ 1
- 0
packages/tldraw/src/components/Divider/index.ts View File

1
+export * from './Divider'

+ 5
- 0
packages/tldraw/src/components/DropdownMenu/DMArrow.tsx View File

1
+import { Arrow } from '@radix-ui/react-dropdown-menu'
2
+import { breakpoints } from '~components/breakpoints'
3
+import styled from '~styles/stitches.config'
4
+
5
+export const DMArrow = styled(Arrow, { fill: '$panel', bp: breakpoints })

+ 33
- 0
packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx View File

1
+import * as React from 'react'
2
+import { CheckboxItem } from '@radix-ui/react-dropdown-menu'
3
+import { RowButton } from '~components/RowButton'
4
+
5
+interface DMCheckboxItemProps {
6
+  checked: boolean
7
+  disabled?: boolean
8
+  onCheckedChange: (isChecked: boolean) => void
9
+  children: React.ReactNode
10
+  kbd?: string
11
+}
12
+
13
+export function DMCheckboxItem({
14
+  checked,
15
+  disabled = false,
16
+  onCheckedChange,
17
+  kbd,
18
+  children,
19
+}: DMCheckboxItemProps): JSX.Element {
20
+  return (
21
+    <CheckboxItem
22
+      dir="ltr"
23
+      onCheckedChange={onCheckedChange}
24
+      checked={checked}
25
+      disabled={disabled}
26
+      asChild
27
+    >
28
+      <RowButton kbd={kbd} hasIndicator>
29
+        {children}
30
+      </RowButton>
31
+    </CheckboxItem>
32
+  )
33
+}

+ 36
- 0
packages/tldraw/src/components/DropdownMenu/DMContent.tsx View File

1
+import * as React from 'react'
2
+import { Content } from '@radix-ui/react-dropdown-menu'
3
+import styled from '~styles/stitches.config'
4
+import { MenuContent } from '~components/MenuContent'
5
+
6
+export interface DMContentProps {
7
+  variant?: 'grid' | 'menu'
8
+  align?: 'start' | 'center' | 'end'
9
+  children: React.ReactNode
10
+}
11
+
12
+export function DMContent({ children, align, variant }: DMContentProps): JSX.Element {
13
+  return (
14
+    <Content sideOffset={8} dir="ltr" asChild align={align}>
15
+      <StyledContent variant={variant}>{children}</StyledContent>
16
+    </Content>
17
+  )
18
+}
19
+
20
+export const StyledContent = styled(MenuContent, {
21
+  width: 'fit-content',
22
+  height: 'fit-content',
23
+  minWidth: 0,
24
+  variants: {
25
+    variant: {
26
+      grid: {
27
+        display: 'grid',
28
+        gridTemplateColumns: 'repeat(4, auto)',
29
+        gap: 0,
30
+      },
31
+      menu: {
32
+        minWidth: 128,
33
+      },
34
+    },
35
+  },
36
+})

+ 11
- 0
packages/tldraw/src/components/DropdownMenu/DMDivider.tsx View File

1
+import { Separator } from '@radix-ui/react-dropdown-menu'
2
+import styled from '~styles/stitches.config'
3
+
4
+export const DMDivider = styled(Separator, {
5
+  backgroundColor: '$hover',
6
+  height: 1,
7
+  marginTop: '$2',
8
+  marginRight: '-$2',
9
+  marginBottom: '$2',
10
+  marginLeft: '-$2',
11
+})

+ 23
- 0
packages/tldraw/src/components/DropdownMenu/DMIconButton.tsx View File

1
+import * as React from 'react'
2
+import { Item } from '@radix-ui/react-dropdown-menu'
3
+import { IconButton } from '~components/IconButton/IconButton'
4
+
5
+interface DMIconButtonProps {
6
+  onSelect: () => void
7
+  disabled?: boolean
8
+  children: React.ReactNode
9
+}
10
+
11
+export function DMIconButton({
12
+  onSelect,
13
+  children,
14
+  disabled = false,
15
+}: DMIconButtonProps): JSX.Element {
16
+  return (
17
+    <Item dir="ltr" asChild>
18
+      <IconButton disabled={disabled} onSelect={onSelect}>
19
+        {children}
20
+      </IconButton>
21
+    </Item>
22
+  )
23
+}

+ 11
- 0
packages/tldraw/src/components/DropdownMenu/DMItem.tsx View File

1
+import * as React from 'react'
2
+import { Item } from '@radix-ui/react-dropdown-menu'
3
+import { RowButton, RowButtonProps } from '~components/RowButton'
4
+
5
+export function DMItem({ onSelect, ...rest }: RowButtonProps): JSX.Element {
6
+  return (
7
+    <Item dir="ltr" asChild onSelect={onSelect}>
8
+      <RowButton {...rest} />
9
+    </Item>
10
+  )
11
+}

+ 26
- 0
packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx View File

1
+import { RadioItem } from '@radix-ui/react-dropdown-menu'
2
+import styled from '~styles/stitches.config'
3
+
4
+export const DMRadioItem = styled(RadioItem, {
5
+  height: '32px',
6
+  width: '32px',
7
+  backgroundColor: '$panel',
8
+  borderRadius: '4px',
9
+  padding: '0',
10
+  margin: '0',
11
+  display: 'flex',
12
+  alignItems: 'center',
13
+  justifyContent: 'center',
14
+  outline: 'none',
15
+  border: 'none',
16
+  pointerEvents: 'all',
17
+  cursor: 'pointer',
18
+
19
+  '&:focus': {
20
+    backgroundColor: '$hover',
21
+  },
22
+
23
+  '&:hover:not(:disabled)': {
24
+    backgroundColor: '$hover',
25
+  },
26
+})

+ 28
- 0
packages/tldraw/src/components/DropdownMenu/DMSubMenu.tsx View File

1
+import * as React from 'react'
2
+import { Root, TriggerItem, Content, Arrow } from '@radix-ui/react-dropdown-menu'
3
+import { RowButton } from '~components/RowButton'
4
+import { MenuContent } from '~components/MenuContent'
5
+
6
+export interface DMSubMenuProps {
7
+  label: string
8
+  disabled?: boolean
9
+  children: React.ReactNode
10
+}
11
+
12
+export function DMSubMenu({ children, disabled = false, label }: DMSubMenuProps): JSX.Element {
13
+  return (
14
+    <Root dir="ltr">
15
+      <TriggerItem dir="ltr" asChild>
16
+        <RowButton disabled={disabled} hasArrow>
17
+          {label}
18
+        </RowButton>
19
+      </TriggerItem>
20
+      <Content dir="ltr" asChild sideOffset={2} alignOffset={-2}>
21
+        <MenuContent>
22
+          {children}
23
+          <Arrow offset={13} />
24
+        </MenuContent>
25
+      </Content>
26
+    </Root>
27
+  )
28
+}

+ 15
- 0
packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx View File

1
+import * as React from 'react'
2
+import { Trigger } from '@radix-ui/react-dropdown-menu'
3
+import { ToolButton } from '~components/ToolButton'
4
+
5
+interface DMTriggerIconProps {
6
+  children: React.ReactNode
7
+}
8
+
9
+export function DMTriggerIcon({ children }: DMTriggerIconProps) {
10
+  return (
11
+    <Trigger asChild>
12
+      <ToolButton>{children}</ToolButton>
13
+    </Trigger>
14
+  )
15
+}

+ 9
- 0
packages/tldraw/src/components/DropdownMenu/index.tsx View File

1
+export * from './DMArrow'
2
+export * from './DMItem'
3
+export * from './DMCheckboxItem'
4
+export * from './DMContent'
5
+export * from './DMDivider'
6
+export * from './DMIconButton'
7
+export * from './DMRadioItem'
8
+export * from './DMSubMenu'
9
+export * from './DMTriggerIcon'

+ 32
- 0
packages/tldraw/src/components/FocusButton/FocusButton.tsx View File

1
+import { DotFilledIcon } from '@radix-ui/react-icons'
2
+import * as React from 'react'
3
+import { IconButton } from '~components/IconButton/IconButton'
4
+import styled from '~styles'
5
+
6
+interface FocusButtonProps {
7
+  onSelect: () => void
8
+}
9
+
10
+export function FocusButton({ onSelect }: FocusButtonProps) {
11
+  return (
12
+    <StyledButtonContainer>
13
+      <IconButton onClick={onSelect}>
14
+        <DotFilledIcon />
15
+      </IconButton>
16
+    </StyledButtonContainer>
17
+  )
18
+}
19
+
20
+const StyledButtonContainer = styled('div', {
21
+  opacity: 1,
22
+  zIndex: 100,
23
+  backgroundColor: 'transparent',
24
+
25
+  '& svg': {
26
+    color: '$muted',
27
+  },
28
+
29
+  '&:hover svg': {
30
+    color: '$text',
31
+  },
32
+})

+ 0
- 0
packages/tldraw/src/components/FocusButton/index.ts View File


packages/tldraw/src/components/shared/icon-button.tsx → packages/tldraw/src/components/IconButton/IconButton.tsx View File

1
-import css from '~styles'
1
+import styled from '~styles'
2
 
2
 
3
-/* -------------------------------------------------- */
4
-/*                     Icon Button                    */
5
-/* -------------------------------------------------- */
6
-
7
-export const iconButton = css({
3
+export const IconButton = styled('button', {
8
   position: 'relative',
4
   position: 'relative',
9
   height: '32px',
5
   height: '32px',
10
   width: '32px',
6
   width: '32px',
12
   borderRadius: '4px',
8
   borderRadius: '4px',
13
   padding: '0',
9
   padding: '0',
14
   margin: '0',
10
   margin: '0',
15
-  display: 'grid',
16
-  alignItems: 'center',
17
-  justifyContent: 'center',
18
   outline: 'none',
11
   outline: 'none',
19
   border: 'none',
12
   border: 'none',
20
   pointerEvents: 'all',
13
   pointerEvents: 'all',
21
   fontSize: '$0',
14
   fontSize: '$0',
22
   color: '$text',
15
   color: '$text',
23
   cursor: 'pointer',
16
   cursor: 'pointer',
17
+  display: 'grid',
18
+  alignItems: 'center',
19
+  justifyContent: 'center',
24
 
20
 
25
   '& > *': {
21
   '& > *': {
26
     gridRow: 1,
22
     gridRow: 1,

+ 1
- 0
packages/tldraw/src/components/IconButton/index.ts View File

1
+export * from './IconButton'

packages/tldraw/src/components/shared/kbd.tsx → packages/tldraw/src/components/Kbd/Kbd.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
-import css from '~styles'
2
+import styled from '~styles'
3
 import { Utils } from '@tldraw/core'
3
 import { Utils } from '@tldraw/core'
4
 
4
 
5
 /* -------------------------------------------------- */
5
 /* -------------------------------------------------- */
6
 /*                  Keyboard Shortcut                 */
6
 /*                  Keyboard Shortcut                 */
7
 /* -------------------------------------------------- */
7
 /* -------------------------------------------------- */
8
 
8
 
9
-export function commandKey(): string {
10
-  return Utils.isDarwin() ? '⌘' : 'Ctrl'
11
-}
9
+const commandKey = () => (Utils.isDarwin() ? '⌘' : 'Ctrl')
12
 
10
 
13
 export function Kbd({
11
 export function Kbd({
14
   variant,
12
   variant,
18
   children: string
16
   children: string
19
 }): JSX.Element | null {
17
 }): JSX.Element | null {
20
   return (
18
   return (
21
-    <kbd className={kbd({ variant })}>
19
+    <StyledKbd variant={variant}>
22
       {children
20
       {children
23
         .replaceAll('#', commandKey())
21
         .replaceAll('#', commandKey())
24
         .split('')
22
         .split('')
25
         .map((k, i) => (
23
         .map((k, i) => (
26
           <span key={i}>{k}</span>
24
           <span key={i}>{k}</span>
27
         ))}
25
         ))}
28
-    </kbd>
26
+    </StyledKbd>
29
   )
27
   )
30
 }
28
 }
31
 
29
 
32
-export const kbd = css({
30
+export const StyledKbd = styled('kbd', {
33
   marginLeft: '$3',
31
   marginLeft: '$3',
34
   textShadow: '$2',
32
   textShadow: '$2',
35
   textAlign: 'center',
33
   textAlign: 'center',

+ 1
- 0
packages/tldraw/src/components/Kbd/index.ts View File

1
+export * from './Kbd'

+ 17
- 0
packages/tldraw/src/components/MenuContent/MenuContent.ts View File

1
+import styled from '~styles'
2
+
3
+export const MenuContent = styled('div', {
4
+  position: 'relative',
5
+  overflow: 'hidden',
6
+  userSelect: 'none',
7
+  display: 'flex',
8
+  flexDirection: 'column',
9
+  zIndex: 180,
10
+  minWidth: 180,
11
+  pointerEvents: 'all',
12
+  backgroundColor: '$panel',
13
+  boxShadow: '$panel',
14
+  padding: '$2 $2',
15
+  borderRadius: '$3',
16
+  font: '$ui',
17
+})

+ 1
- 0
packages/tldraw/src/components/MenuContent/index.ts View File

1
+export * from './MenuContent'

+ 30
- 0
packages/tldraw/src/components/Panel/Panel.tsx View File

1
+import styled from '~styles/stitches.config'
2
+
3
+export const Panel = styled('div', {
4
+  backgroundColor: '$panel',
5
+  display: 'flex',
6
+  flexDirection: 'row',
7
+  padding: '0 $2',
8
+  boxShadow: '$panel',
9
+  variants: {
10
+    side: {
11
+      center: {
12
+        borderTopLeftRadius: '$4',
13
+        borderTopRightRadius: '$4',
14
+        // borderTop: '1px solid $panelBorder',
15
+        // borderLeft: '1px solid $panelBorder',
16
+        // borderRight: '1px solid $panelBorder',
17
+      },
18
+      left: {
19
+        borderBottomRightRadius: '$4',
20
+        // borderBottom: '1px solid $panelBorder',
21
+        // borderRight: '1px solid $panelBorder',
22
+      },
23
+      right: {
24
+        borderBottomLeftRadius: '$4',
25
+        // borderBottom: '1px solid $panelBorder',
26
+        // borderLeft: '1px solid $panelBorder',
27
+      },
28
+    },
29
+  },
30
+})

+ 1
- 0
packages/tldraw/src/components/Panel/index.ts View File

1
+export * from './Panel'

+ 142
- 0
packages/tldraw/src/components/RowButton/RowButton.tsx View File

1
+import { ItemIndicator } from '@radix-ui/react-dropdown-menu'
2
+import { ChevronRightIcon, CheckIcon } from '@radix-ui/react-icons'
3
+import * as React from 'react'
4
+import { breakpoints } from '~components/breakpoints'
5
+import { Kbd } from '~components/Kbd'
6
+import { SmallIcon } from '~components/SmallIcon'
7
+import styled from '~styles'
8
+
9
+export interface RowButtonProps {
10
+  onSelect?: () => void
11
+  children: React.ReactNode
12
+  disabled?: boolean
13
+  kbd?: string
14
+  isActive?: boolean
15
+  isWarning?: boolean
16
+  hasIndicator?: boolean
17
+  hasArrow?: boolean
18
+}
19
+
20
+export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(
21
+  (
22
+    {
23
+      onSelect,
24
+      isActive = false,
25
+      isWarning = false,
26
+      hasIndicator = false,
27
+      hasArrow = false,
28
+      disabled = false,
29
+      kbd,
30
+      children,
31
+      ...rest
32
+    },
33
+    ref
34
+  ) => {
35
+    return (
36
+      <StyledRowButton
37
+        ref={ref}
38
+        bp={breakpoints}
39
+        isWarning={isWarning}
40
+        isActive={isActive}
41
+        disabled={disabled}
42
+        onPointerDown={onSelect}
43
+        {...rest}
44
+      >
45
+        <StyledRowButtonInner>
46
+          {children}
47
+          {kbd ? <Kbd variant="menu">{kbd}</Kbd> : undefined}
48
+          {hasIndicator && (
49
+            <ItemIndicator dir="ltr">
50
+              <SmallIcon>
51
+                <CheckIcon />
52
+              </SmallIcon>
53
+            </ItemIndicator>
54
+          )}
55
+          {hasArrow && (
56
+            <SmallIcon>
57
+              <ChevronRightIcon />
58
+            </SmallIcon>
59
+          )}
60
+        </StyledRowButtonInner>
61
+      </StyledRowButton>
62
+    )
63
+  }
64
+)
65
+
66
+const StyledRowButtonInner = styled('div', {
67
+  height: '100%',
68
+  width: '100%',
69
+  color: '$text',
70
+  fontFamily: '$ui',
71
+  fontWeight: 400,
72
+  fontSize: '$1',
73
+  backgroundColor: '$panel',
74
+  borderRadius: '$2',
75
+  display: 'flex',
76
+  flexDirection: 'row',
77
+  alignItems: 'center',
78
+  padding: '0 $3',
79
+  justifyContent: 'space-between',
80
+  border: '1px solid transparent',
81
+
82
+  '& svg': {
83
+    position: 'relative',
84
+    stroke: '$overlay',
85
+    strokeWidth: 1,
86
+    zIndex: 1,
87
+  },
88
+})
89
+
90
+export const StyledRowButton = styled('button', {
91
+  position: 'relative',
92
+  width: '100%',
93
+  background: 'none',
94
+  border: 'none',
95
+  cursor: 'pointer',
96
+  height: '32px',
97
+  outline: 'none',
98
+  borderRadius: 4,
99
+  userSelect: 'none',
100
+  margin: 0,
101
+  padding: '0 0',
102
+
103
+  '&[data-disabled]': {
104
+    opacity: 0.3,
105
+  },
106
+
107
+  '&:disabled': {
108
+    opacity: 0.3,
109
+  },
110
+
111
+  variants: {
112
+    bp: {
113
+      mobile: {},
114
+      small: {},
115
+    },
116
+    size: {
117
+      icon: {
118
+        padding: '4px ',
119
+        width: 'auto',
120
+      },
121
+    },
122
+    isWarning: {
123
+      true: {
124
+        color: '$warn',
125
+      },
126
+    },
127
+    isActive: {
128
+      true: {
129
+        backgroundColor: '$hover',
130
+      },
131
+      false: {
132
+        [`&:hover:not(:disabled) ${StyledRowButtonInner}`]: {
133
+          backgroundColor: '$hover',
134
+          border: '1px solid $panel',
135
+          '& *[data-shy="true"]': {
136
+            opacity: 1,
137
+          },
138
+        },
139
+      },
140
+    },
141
+  },
142
+})

+ 1
- 0
packages/tldraw/src/components/RowButton/index.ts View File

1
+export * from './RowButton'

+ 27
- 0
packages/tldraw/src/components/SmallIcon/SmallIcon.tsx View File

1
+import styled from '~styles'
2
+
3
+export const SmallIcon = styled('div', {
4
+  height: '100%',
5
+  borderRadius: '4px',
6
+  marginRight: '1px',
7
+  width: 'fit-content',
8
+  display: 'grid',
9
+  alignItems: 'center',
10
+  justifyContent: 'center',
11
+  outline: 'none',
12
+  border: 'none',
13
+  pointerEvents: 'all',
14
+  cursor: 'pointer',
15
+  color: '$text',
16
+
17
+  '& svg': {
18
+    height: 16,
19
+    width: 16,
20
+    strokeWidth: 1,
21
+  },
22
+
23
+  '& > *': {
24
+    gridRow: 1,
25
+    gridColumn: 1,
26
+  },
27
+})

+ 1
- 0
packages/tldraw/src/components/SmallIcon/index.ts View File

1
+export * from './SmallIcon'

+ 132
- 0
packages/tldraw/src/components/ToolButton/ToolButton.tsx View File

1
+import * as React from 'react'
2
+import { Tooltip } from '~components/Tooltip'
3
+import styled from '~styles'
4
+
5
+export interface ToolButtonProps {
6
+  onSelect?: () => void
7
+  onDoubleClick?: () => void
8
+  isActive?: boolean
9
+  variant?: 'icon' | 'text' | 'circle' | 'primary'
10
+  children: React.ReactNode
11
+}
12
+
13
+export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
14
+  ({ onSelect, onDoubleClick, isActive = false, variant, children, ...rest }, ref) => {
15
+    return (
16
+      <StyledToolButton
17
+        ref={ref}
18
+        isActive={isActive}
19
+        variant={variant}
20
+        onPointerDown={onSelect}
21
+        onDoubleClick={onDoubleClick}
22
+        {...rest}
23
+      >
24
+        <StyledToolButtonInner isActive={isActive} variant={variant}>
25
+          {children}
26
+        </StyledToolButtonInner>
27
+      </StyledToolButton>
28
+    )
29
+  }
30
+)
31
+
32
+/* ------------------ With Tooltip ------------------ */
33
+
34
+interface ToolButtonWithTooltipProps extends ToolButtonProps {
35
+  label: string
36
+  kbd?: string
37
+}
38
+
39
+export function ToolButtonWithTooltip({ label, kbd, ...rest }: ToolButtonWithTooltipProps) {
40
+  return (
41
+    <Tooltip label={label[0].toUpperCase() + label.slice(1)} kbd={kbd}>
42
+      <ToolButton variant="primary" {...rest} />
43
+    </Tooltip>
44
+  )
45
+}
46
+
47
+export const StyledToolButtonInner = styled('div', {
48
+  position: 'relative',
49
+  height: '100%',
50
+  width: '100%',
51
+  color: '$text',
52
+  backgroundColor: '$panel',
53
+  borderRadius: '$2',
54
+  margin: '0',
55
+  display: 'flex',
56
+  alignItems: 'center',
57
+  justifyContent: 'center',
58
+  fontFamily: '$ui',
59
+  userSelect: 'none',
60
+  boxSizing: 'border-box',
61
+  border: '1px solid transparent',
62
+
63
+  variants: {
64
+    variant: {
65
+      primary: {
66
+        '& svg': {
67
+          width: 20,
68
+          height: 20,
69
+        },
70
+      },
71
+      icon: {
72
+        display: 'grid',
73
+        '& > *': {
74
+          gridRow: 1,
75
+          gridColumn: 1,
76
+        },
77
+      },
78
+      text: {
79
+        fontSize: '$1',
80
+        padding: '0 $3',
81
+      },
82
+      circle: {
83
+        borderRadius: '100%',
84
+        boxShadow: '$panel',
85
+      },
86
+    },
87
+    isActive: {
88
+      true: {
89
+        backgroundColor: '$selected',
90
+        color: '$panelActive',
91
+      },
92
+    },
93
+  },
94
+})
95
+
96
+export const StyledToolButton = styled('button', {
97
+  position: 'relative',
98
+  color: '$text',
99
+  height: '48px',
100
+  width: '40px',
101
+  fontSize: '$0',
102
+  background: 'none',
103
+  margin: '0',
104
+  padding: '$3 $2',
105
+  display: 'flex',
106
+  alignItems: 'center',
107
+  justifyContent: 'center',
108
+  outline: 'none',
109
+  cursor: 'pointer',
110
+  pointerEvents: 'all',
111
+  border: 'none',
112
+
113
+  variants: {
114
+    variant: {
115
+      primary: {},
116
+      icon: {},
117
+      text: {
118
+        width: 'auto',
119
+      },
120
+      circle: {},
121
+    },
122
+    isActive: {
123
+      true: {},
124
+      false: {
125
+        [`&:hover:not(:disabled) ${StyledToolButtonInner}`]: {
126
+          backgroundColor: '$hover',
127
+          border: '1px solid $panel',
128
+        },
129
+      },
130
+    },
131
+  },
132
+})

+ 1
- 0
packages/tldraw/src/components/ToolButton/index.ts View File

1
+export * from './ToolButton'

+ 289
- 0
packages/tldraw/src/components/ToolsPanel/ActionButton.tsx View File

1
+import * as React from 'react'
2
+import { Tooltip } from '~components/Tooltip/Tooltip'
3
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
4
+import { useTLDrawContext } from '~hooks'
5
+import styled from '~styles'
6
+import { AlignType, Data, DistributeType, StretchType } from '~types'
7
+import {
8
+  ArrowDownIcon,
9
+  ArrowUpIcon,
10
+  AspectRatioIcon,
11
+  CopyIcon,
12
+  DotsHorizontalIcon,
13
+  GroupIcon,
14
+  LockClosedIcon,
15
+  LockOpen1Icon,
16
+  PinBottomIcon,
17
+  PinTopIcon,
18
+  RotateCounterClockwiseIcon,
19
+  AlignBottomIcon,
20
+  AlignCenterHorizontallyIcon,
21
+  AlignCenterVerticallyIcon,
22
+  AlignLeftIcon,
23
+  AlignRightIcon,
24
+  AlignTopIcon,
25
+  SpaceEvenlyHorizontallyIcon,
26
+  SpaceEvenlyVerticallyIcon,
27
+  StretchHorizontallyIcon,
28
+  StretchVerticallyIcon,
29
+} from '@radix-ui/react-icons'
30
+import { DMContent } from '~components/DropdownMenu'
31
+import { Divider } from '~components/Divider'
32
+import { TrashIcon } from '~components/icons'
33
+import { IconButton } from '~components/IconButton'
34
+import { ToolButton } from '~components/ToolButton'
35
+
36
+const selectedShapesCountSelector = (s: Data) =>
37
+  s.document.pageStates[s.appState.currentPageId].selectedIds.length
38
+
39
+const isAllLockedSelector = (s: Data) => {
40
+  const page = s.document.pages[s.appState.currentPageId]
41
+  const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
42
+  return selectedIds.every((id) => page.shapes[id].isLocked)
43
+}
44
+
45
+const isAllAspectLockedSelector = (s: Data) => {
46
+  const page = s.document.pages[s.appState.currentPageId]
47
+  const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
48
+  return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
49
+}
50
+
51
+const isAllGroupedSelector = (s: Data) => {
52
+  const page = s.document.pages[s.appState.currentPageId]
53
+  const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map(
54
+    (id) => page.shapes[id]
55
+  )
56
+
57
+  return selectedShapes.every(
58
+    (shape) =>
59
+      shape.children !== undefined ||
60
+      (shape.parentId === selectedShapes[0].parentId &&
61
+        selectedShapes[0].parentId !== s.appState.currentPageId)
62
+  )
63
+}
64
+
65
+const hasSelectionSelector = (s: Data) => {
66
+  const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
67
+  return selectedIds.length > 0
68
+}
69
+
70
+const hasMultipleSelectionSelector = (s: Data) => {
71
+  const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
72
+  return selectedIds.length > 1
73
+}
74
+
75
+export function ActionButton(): JSX.Element {
76
+  const { tlstate, useSelector } = useTLDrawContext()
77
+
78
+  const isAllLocked = useSelector(isAllLockedSelector)
79
+
80
+  const isAllAspectLocked = useSelector(isAllAspectLockedSelector)
81
+
82
+  const isAllGrouped = useSelector(isAllGroupedSelector)
83
+
84
+  const hasSelection = useSelector(hasSelectionSelector)
85
+
86
+  const hasMultipleSelection = useSelector(hasMultipleSelectionSelector)
87
+
88
+  const handleRotate = React.useCallback(() => {
89
+    tlstate.rotate()
90
+  }, [tlstate])
91
+
92
+  const handleDuplicate = React.useCallback(() => {
93
+    tlstate.duplicate()
94
+  }, [tlstate])
95
+
96
+  const handleToggleLocked = React.useCallback(() => {
97
+    tlstate.toggleLocked()
98
+  }, [tlstate])
99
+
100
+  const handleToggleAspectRatio = React.useCallback(() => {
101
+    tlstate.toggleAspectRatioLocked()
102
+  }, [tlstate])
103
+
104
+  const handleGroup = React.useCallback(() => {
105
+    tlstate.group()
106
+  }, [tlstate])
107
+
108
+  const handleMoveToBack = React.useCallback(() => {
109
+    tlstate.moveToBack()
110
+  }, [tlstate])
111
+
112
+  const handleMoveBackward = React.useCallback(() => {
113
+    tlstate.moveBackward()
114
+  }, [tlstate])
115
+
116
+  const handleMoveForward = React.useCallback(() => {
117
+    tlstate.moveForward()
118
+  }, [tlstate])
119
+
120
+  const handleMoveToFront = React.useCallback(() => {
121
+    tlstate.moveToFront()
122
+  }, [tlstate])
123
+
124
+  const handleDelete = React.useCallback(() => {
125
+    tlstate.delete()
126
+  }, [tlstate])
127
+
128
+  const alignTop = React.useCallback(() => {
129
+    tlstate.align(AlignType.Top)
130
+  }, [tlstate])
131
+
132
+  const alignCenterVertical = React.useCallback(() => {
133
+    tlstate.align(AlignType.CenterVertical)
134
+  }, [tlstate])
135
+
136
+  const alignBottom = React.useCallback(() => {
137
+    tlstate.align(AlignType.Bottom)
138
+  }, [tlstate])
139
+
140
+  const stretchVertically = React.useCallback(() => {
141
+    tlstate.stretch(StretchType.Vertical)
142
+  }, [tlstate])
143
+
144
+  const distributeVertically = React.useCallback(() => {
145
+    tlstate.distribute(DistributeType.Vertical)
146
+  }, [tlstate])
147
+
148
+  const alignLeft = React.useCallback(() => {
149
+    tlstate.align(AlignType.Left)
150
+  }, [tlstate])
151
+
152
+  const alignCenterHorizontal = React.useCallback(() => {
153
+    tlstate.align(AlignType.CenterHorizontal)
154
+  }, [tlstate])
155
+
156
+  const alignRight = React.useCallback(() => {
157
+    tlstate.align(AlignType.Right)
158
+  }, [tlstate])
159
+
160
+  const stretchHorizontally = React.useCallback(() => {
161
+    tlstate.stretch(StretchType.Horizontal)
162
+  }, [tlstate])
163
+
164
+  const distributeHorizontally = React.useCallback(() => {
165
+    tlstate.distribute(DistributeType.Horizontal)
166
+  }, [tlstate])
167
+
168
+  const selectedShapesCount = useSelector(selectedShapesCountSelector)
169
+
170
+  const hasTwoOrMore = selectedShapesCount > 1
171
+  const hasThreeOrMore = selectedShapesCount > 2
172
+
173
+  return (
174
+    <DropdownMenu.Root dir="ltr">
175
+      <DropdownMenu.Trigger dir="ltr" asChild>
176
+        <ToolButton variant="circle">
177
+          <DotsHorizontalIcon />
178
+        </ToolButton>
179
+      </DropdownMenu.Trigger>
180
+      <DMContent>
181
+        <>
182
+          <ButtonsRow>
183
+            <IconButton disabled={!hasSelection} onSelect={handleDuplicate}>
184
+              <Tooltip label="Duplicate" kbd={`#D`}>
185
+                <CopyIcon />
186
+              </Tooltip>
187
+            </IconButton>
188
+            <IconButton disabled={!hasSelection} onSelect={handleRotate}>
189
+              <Tooltip label="Rotate">
190
+                <RotateCounterClockwiseIcon />
191
+              </Tooltip>
192
+            </IconButton>
193
+            <IconButton disabled={!hasSelection} onSelect={handleToggleLocked}>
194
+              <Tooltip label="Toogle Locked" kbd={`#L`}>
195
+                {isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon opacity={0.4} />}
196
+              </Tooltip>
197
+            </IconButton>
198
+            <IconButton disabled={!hasSelection} onSelect={handleToggleAspectRatio}>
199
+              <Tooltip label="Toogle Aspect Ratio Lock">
200
+                <AspectRatioIcon opacity={isAllAspectLocked ? 1 : 0.4} />
201
+              </Tooltip>
202
+            </IconButton>
203
+            <IconButton disabled={!isAllGrouped && !hasMultipleSelection} onSelect={handleGroup}>
204
+              <Tooltip label="Group" kbd={`#G`}>
205
+                <GroupIcon opacity={isAllGrouped ? 1 : 0.4} />
206
+              </Tooltip>
207
+            </IconButton>
208
+          </ButtonsRow>
209
+          <ButtonsRow>
210
+            <IconButton disabled={!hasSelection} onSelect={handleMoveToBack}>
211
+              <Tooltip label="Move to Back" kbd={`#⇧[`}>
212
+                <PinBottomIcon />
213
+              </Tooltip>
214
+            </IconButton>
215
+
216
+            <IconButton disabled={!hasSelection} onSelect={handleMoveBackward}>
217
+              <Tooltip label="Move Backward" kbd={`#[`}>
218
+                <ArrowDownIcon />
219
+              </Tooltip>
220
+            </IconButton>
221
+            <IconButton disabled={!hasSelection} onSelect={handleMoveForward}>
222
+              <Tooltip label="Move Forward" kbd={`#]`}>
223
+                <ArrowUpIcon />
224
+              </Tooltip>
225
+            </IconButton>
226
+            <IconButton disabled={!hasSelection} onSelect={handleMoveToFront}>
227
+              <Tooltip label="More to Front" kbd={`#⇧]`}>
228
+                <PinTopIcon />
229
+              </Tooltip>
230
+            </IconButton>
231
+            <IconButton disabled={!hasSelection} onSelect={handleDelete}>
232
+              <Tooltip label="Delete" kbd="⌫">
233
+                <TrashIcon />
234
+              </Tooltip>
235
+            </IconButton>
236
+          </ButtonsRow>
237
+          <Divider />
238
+          <ButtonsRow>
239
+            <IconButton disabled={!hasTwoOrMore} onSelect={alignLeft}>
240
+              <AlignLeftIcon />
241
+            </IconButton>
242
+            <IconButton disabled={!hasTwoOrMore} onSelect={alignCenterHorizontal}>
243
+              <AlignCenterHorizontallyIcon />
244
+            </IconButton>
245
+            <IconButton disabled={!hasTwoOrMore} onSelect={alignRight}>
246
+              <AlignRightIcon />
247
+            </IconButton>
248
+            <IconButton disabled={!hasTwoOrMore} onSelect={stretchHorizontally}>
249
+              <StretchHorizontallyIcon />
250
+            </IconButton>
251
+            <IconButton disabled={!hasThreeOrMore} onSelect={distributeHorizontally}>
252
+              <SpaceEvenlyHorizontallyIcon />
253
+            </IconButton>
254
+          </ButtonsRow>
255
+          <ButtonsRow>
256
+            <IconButton disabled={!hasTwoOrMore} onSelect={alignTop}>
257
+              <AlignTopIcon />
258
+            </IconButton>
259
+            <IconButton disabled={!hasTwoOrMore} onSelect={alignCenterVertical}>
260
+              <AlignCenterVerticallyIcon />
261
+            </IconButton>
262
+            <IconButton disabled={!hasTwoOrMore} onSelect={alignBottom}>
263
+              <AlignBottomIcon />
264
+            </IconButton>
265
+            <IconButton disabled={!hasTwoOrMore} onSelect={stretchVertically}>
266
+              <StretchVerticallyIcon />
267
+            </IconButton>
268
+            <IconButton disabled={!hasThreeOrMore} onSelect={distributeVertically}>
269
+              <SpaceEvenlyVerticallyIcon />
270
+            </IconButton>
271
+          </ButtonsRow>
272
+        </>
273
+      </DMContent>
274
+    </DropdownMenu.Root>
275
+  )
276
+}
277
+
278
+export const ButtonsRow = styled('div', {
279
+  position: 'relative',
280
+  display: 'flex',
281
+  width: '100%',
282
+  background: 'none',
283
+  border: 'none',
284
+  cursor: 'pointer',
285
+  outline: 'none',
286
+  alignItems: 'center',
287
+  justifyContent: 'flex-start',
288
+  padding: 0,
289
+})

packages/tldraw/src/components/tools-panel/back-to-content/back-to-content.tsx → packages/tldraw/src/components/ToolsPanel/BackToContent.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
-import { floatingContainer, rowButton } from '~components/shared'
3
-import css from '~styles'
2
+import styled from '~styles'
4
 import type { Data } from '~types'
3
 import type { Data } from '~types'
5
 import { useTLDrawContext } from '~hooks'
4
 import { useTLDrawContext } from '~hooks'
5
+import { RowButton } from '~components/RowButton'
6
+import { MenuContent } from '~components/MenuContent'
6
 
7
 
7
 const isEmptyCanvasSelector = (s: Data) =>
8
 const isEmptyCanvasSelector = (s: Data) =>
8
   Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
9
   Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
16
   if (!isEmptyCanvas) return null
17
   if (!isEmptyCanvas) return null
17
 
18
 
18
   return (
19
   return (
19
-    <div className={backToContentButton()}>
20
-      <button className={rowButton()} onClick={tlstate.zoomToContent}>
21
-        Back to content
22
-      </button>
23
-    </div>
20
+    <BackToContentContainer>
21
+      <RowButton onSelect={tlstate.zoomToContent}>Back to content</RowButton>
22
+    </BackToContentContainer>
24
   )
23
   )
25
 })
24
 })
26
 
25
 
27
-const backToContentButton = css(floatingContainer, {
26
+const BackToContentContainer = styled(MenuContent, {
28
   pointerEvents: 'all',
27
   pointerEvents: 'all',
29
   width: 'fit-content',
28
   width: 'fit-content',
29
+  minWidth: 0,
30
   gridRow: 1,
30
   gridRow: 1,
31
   flexGrow: 2,
31
   flexGrow: 2,
32
   display: 'block',
32
   display: 'block',

+ 22
- 0
packages/tldraw/src/components/ToolsPanel/LockButton.tsx View File

1
+import * as React from 'react'
2
+import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons'
3
+import { Tooltip } from '~components/Tooltip'
4
+import { useTLDrawContext } from '~hooks'
5
+import { ToolButton } from '~components/ToolButton'
6
+import type { Data } from '~types'
7
+
8
+const isToolLockedSelector = (s: Data) => s.appState.isToolLocked
9
+
10
+export function LockButton(): JSX.Element {
11
+  const { tlstate, useSelector } = useTLDrawContext()
12
+
13
+  const isToolLocked = useSelector(isToolLockedSelector)
14
+
15
+  return (
16
+    <Tooltip label="Lock Tool" kbd="7">
17
+      <ToolButton variant="circle" isActive={isToolLocked} onSelect={tlstate.toggleToolLock}>
18
+        {isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
19
+      </ToolButton>
20
+    </Tooltip>
21
+  )
22
+}

packages/tldraw/src/components/tools-panel/primary-tools/primary-tools.tsx → packages/tldraw/src/components/ToolsPanel/PrimaryTools.tsx View File

2
 import {
2
 import {
3
   ArrowTopRightIcon,
3
   ArrowTopRightIcon,
4
   CircleIcon,
4
   CircleIcon,
5
+  CursorArrowIcon,
5
   Pencil1Icon,
6
   Pencil1Icon,
6
   Pencil2Icon,
7
   Pencil2Icon,
7
   SquareIcon,
8
   SquareIcon,
9
 } from '@radix-ui/react-icons'
10
 } from '@radix-ui/react-icons'
10
 import { Data, TLDrawShapeType } from '~types'
11
 import { Data, TLDrawShapeType } from '~types'
11
 import { useTLDrawContext } from '~hooks'
12
 import { useTLDrawContext } from '~hooks'
12
-import { floatingContainer } from '~components/shared'
13
-import { PrimaryButton } from '~components/tools-panel/styled'
13
+import { ToolButtonWithTooltip } from '~components/ToolButton'
14
+import { Panel } from '~components/Panel'
14
 
15
 
15
 const activeToolSelector = (s: Data) => s.appState.activeTool
16
 const activeToolSelector = (s: Data) => s.appState.activeTool
16
 
17
 
19
 
20
 
20
   const activeTool = useSelector(activeToolSelector)
21
   const activeTool = useSelector(activeToolSelector)
21
 
22
 
23
+  const selectSelectTool = React.useCallback(() => {
24
+    tlstate.selectTool('select')
25
+  }, [tlstate])
26
+
22
   const selectDrawTool = React.useCallback(() => {
27
   const selectDrawTool = React.useCallback(() => {
23
     tlstate.selectTool(TLDrawShapeType.Draw)
28
     tlstate.selectTool(TLDrawShapeType.Draw)
24
   }, [tlstate])
29
   }, [tlstate])
44
   }, [tlstate])
49
   }, [tlstate])
45
 
50
 
46
   return (
51
   return (
47
-    <div className={floatingContainer()}>
48
-      <PrimaryButton
52
+    <Panel side="center">
53
+      <ToolButtonWithTooltip
54
+        kbd={'2'}
55
+        label={'select'}
56
+        onSelect={selectSelectTool}
57
+        isActive={activeTool === 'select'}
58
+      >
59
+        <CursorArrowIcon />
60
+      </ToolButtonWithTooltip>
61
+      <ToolButtonWithTooltip
49
         kbd={'2'}
62
         kbd={'2'}
50
         label={TLDrawShapeType.Draw}
63
         label={TLDrawShapeType.Draw}
51
-        onClick={selectDrawTool}
64
+        onSelect={selectDrawTool}
52
         isActive={activeTool === TLDrawShapeType.Draw}
65
         isActive={activeTool === TLDrawShapeType.Draw}
53
       >
66
       >
54
         <Pencil1Icon />
67
         <Pencil1Icon />
55
-      </PrimaryButton>
56
-      <PrimaryButton
68
+      </ToolButtonWithTooltip>
69
+      <ToolButtonWithTooltip
57
         kbd={'3'}
70
         kbd={'3'}
58
         label={TLDrawShapeType.Rectangle}
71
         label={TLDrawShapeType.Rectangle}
59
-        onClick={selectRectangleTool}
72
+        onSelect={selectRectangleTool}
60
         isActive={activeTool === TLDrawShapeType.Rectangle}
73
         isActive={activeTool === TLDrawShapeType.Rectangle}
61
       >
74
       >
62
         <SquareIcon />
75
         <SquareIcon />
63
-      </PrimaryButton>
64
-      <PrimaryButton
76
+      </ToolButtonWithTooltip>
77
+      <ToolButtonWithTooltip
65
         kbd={'4'}
78
         kbd={'4'}
66
         label={TLDrawShapeType.Draw}
79
         label={TLDrawShapeType.Draw}
67
-        onClick={selectEllipseTool}
80
+        onSelect={selectEllipseTool}
68
         isActive={activeTool === TLDrawShapeType.Ellipse}
81
         isActive={activeTool === TLDrawShapeType.Ellipse}
69
       >
82
       >
70
         <CircleIcon />
83
         <CircleIcon />
71
-      </PrimaryButton>
72
-      <PrimaryButton
84
+      </ToolButtonWithTooltip>
85
+      <ToolButtonWithTooltip
73
         kbd={'5'}
86
         kbd={'5'}
74
         label={TLDrawShapeType.Arrow}
87
         label={TLDrawShapeType.Arrow}
75
-        onClick={selectArrowTool}
88
+        onSelect={selectArrowTool}
76
         isActive={activeTool === TLDrawShapeType.Arrow}
89
         isActive={activeTool === TLDrawShapeType.Arrow}
77
       >
90
       >
78
         <ArrowTopRightIcon />
91
         <ArrowTopRightIcon />
79
-      </PrimaryButton>
80
-      <PrimaryButton
92
+      </ToolButtonWithTooltip>
93
+      <ToolButtonWithTooltip
81
         kbd={'6'}
94
         kbd={'6'}
82
         label={TLDrawShapeType.Text}
95
         label={TLDrawShapeType.Text}
83
-        onClick={selectTextTool}
96
+        onSelect={selectTextTool}
84
         isActive={activeTool === TLDrawShapeType.Text}
97
         isActive={activeTool === TLDrawShapeType.Text}
85
       >
98
       >
86
         <TextIcon />
99
         <TextIcon />
87
-      </PrimaryButton>
88
-      <PrimaryButton
100
+      </ToolButtonWithTooltip>
101
+      <ToolButtonWithTooltip
89
         kbd={'7'}
102
         kbd={'7'}
90
         label={TLDrawShapeType.Sticky}
103
         label={TLDrawShapeType.Sticky}
91
-        onClick={selectStickyTool}
104
+        onSelect={selectStickyTool}
92
         isActive={activeTool === TLDrawShapeType.Sticky}
105
         isActive={activeTool === TLDrawShapeType.Sticky}
93
       >
106
       >
94
         <Pencil2Icon />
107
         <Pencil2Icon />
95
-      </PrimaryButton>
96
-    </div>
108
+      </ToolButtonWithTooltip>
109
+    </Panel>
97
   )
110
   )
98
 })
111
 })

packages/tldraw/src/components/tools-panel/status-bar/status-bar.tsx → packages/tldraw/src/components/ToolsPanel/StatusBar.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
 import { useTLDrawContext } from '~hooks'
2
 import { useTLDrawContext } from '~hooks'
3
 import type { Data } from '~types'
3
 import type { Data } from '~types'
4
-import css from '~styles'
4
+import styled from '~styles'
5
+import { breakpoints } from '~components/breakpoints'
5
 
6
 
6
 const statusSelector = (s: Data) => s.appState.status
7
 const statusSelector = (s: Data) => s.appState.status
7
 const activeToolSelector = (s: Data) => s.appState.activeTool
8
 const activeToolSelector = (s: Data) => s.appState.activeTool
12
   const activeTool = useSelector(activeToolSelector)
13
   const activeTool = useSelector(activeToolSelector)
13
 
14
 
14
   return (
15
   return (
15
-    <div className={statusBarContainer({ size: { '@sm': 'small' } })}>
16
-      <div className={section()}>
16
+    <StyledStatusBar bp={breakpoints}>
17
+      <StyledSection>
17
         {activeTool} | {status}
18
         {activeTool} | {status}
18
-      </div>
19
-    </div>
19
+      </StyledSection>
20
+    </StyledStatusBar>
20
   )
21
   )
21
 }
22
 }
22
 
23
 
23
-const statusBarContainer = css({
24
+const StyledStatusBar = styled('div', {
24
   height: 40,
25
   height: 40,
25
   userSelect: 'none',
26
   userSelect: 'none',
26
   borderTop: '1px solid $border',
27
   borderTop: '1px solid $border',
36
   padding: '0 16px',
37
   padding: '0 16px',
37
 
38
 
38
   variants: {
39
   variants: {
39
-    size: {
40
+    bp: {
40
       small: {
41
       small: {
41
         fontSize: '$1',
42
         fontSize: '$1',
42
       },
43
       },
44
   },
45
   },
45
 })
46
 })
46
 
47
 
47
-const section = css({
48
+const StyledSection = styled('div', {
48
   whiteSpace: 'nowrap',
49
   whiteSpace: 'nowrap',
49
   overflow: 'hidden',
50
   overflow: 'hidden',
50
 })
51
 })

packages/tldraw/src/components/tools-panel/tools-panel.test.tsx → packages/tldraw/src/components/ToolsPanel/ToolsPanel.test.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
-import { ToolsPanel } from './tools-panel'
2
+import { ToolsPanel } from './ToolsPanel'
3
 import { renderWithContext } from '~test'
3
 import { renderWithContext } from '~test'
4
 
4
 
5
 describe('tools panel', () => {
5
 describe('tools panel', () => {

+ 78
- 0
packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx View File

1
+import * as React from 'react'
2
+import styled from '~styles'
3
+import type { Data } from '~types'
4
+import { useTLDrawContext } from '~hooks'
5
+import { StatusBar } from './StatusBar'
6
+import { BackToContent } from './BackToContent'
7
+import { PrimaryTools } from './PrimaryTools'
8
+import { ActionButton } from './ActionButton'
9
+import { LockButton } from './LockButton'
10
+
11
+const isDebugModeSelector = (s: Data) => s.settings.isDebugMode
12
+
13
+export const ToolsPanel = React.memo((): JSX.Element => {
14
+  const { useSelector } = useTLDrawContext()
15
+
16
+  const isDebugMode = useSelector(isDebugModeSelector)
17
+
18
+  return (
19
+    <StyledToolsPanelContainer>
20
+      <StyledCenterWrap>
21
+        <BackToContent />
22
+        <StyledPrimaryTools>
23
+          <ActionButton />
24
+          <PrimaryTools />
25
+          <LockButton />
26
+        </StyledPrimaryTools>
27
+      </StyledCenterWrap>
28
+      {isDebugMode && (
29
+        <StyledStatusWrap>
30
+          <StatusBar />
31
+        </StyledStatusWrap>
32
+      )}
33
+    </StyledToolsPanelContainer>
34
+  )
35
+})
36
+
37
+const StyledToolsPanelContainer = styled('div', {
38
+  position: 'absolute',
39
+  bottom: 0,
40
+  left: 0,
41
+  right: 0,
42
+  width: '100%',
43
+  minWidth: 0,
44
+  maxWidth: '100%',
45
+  display: 'grid',
46
+  gridTemplateColumns: 'auto auto auto',
47
+  gridTemplateRows: 'auto auto',
48
+  justifyContent: 'space-between',
49
+  padding: '0',
50
+  alignItems: 'flex-end',
51
+  zIndex: 200,
52
+  pointerEvents: 'none',
53
+  '& > div > *': {
54
+    pointerEvents: 'all',
55
+  },
56
+})
57
+
58
+const StyledCenterWrap = styled('div', {
59
+  gridRow: 1,
60
+  gridColumn: 2,
61
+  display: 'flex',
62
+  width: 'fit-content',
63
+  alignItems: 'center',
64
+  justifyContent: 'center',
65
+  flexDirection: 'column',
66
+  gap: 12,
67
+})
68
+
69
+const StyledStatusWrap = styled('div', {
70
+  gridRow: 2,
71
+  gridColumn: '1 / span 3',
72
+})
73
+
74
+const StyledPrimaryTools = styled('div', {
75
+  position: 'relative',
76
+  display: 'flex',
77
+  gap: '$2',
78
+})

+ 1
- 0
packages/tldraw/src/components/ToolsPanel/index.ts View File

1
+export * from './ToolsPanel'

packages/tldraw/src/components/shared/tooltip.tsx → packages/tldraw/src/components/Tooltip/Tooltip.tsx View File

1
 import * as RadixTooltip from '@radix-ui/react-tooltip'
1
 import * as RadixTooltip from '@radix-ui/react-tooltip'
2
 import * as React from 'react'
2
 import * as React from 'react'
3
-import css from '~styles'
4
-import { Kbd } from './kbd'
3
+import { Kbd } from '~components/Kbd'
4
+import styled from '~styles'
5
 
5
 
6
 /* -------------------------------------------------- */
6
 /* -------------------------------------------------- */
7
 /*                       Tooltip                      */
7
 /*                       Tooltip                      */
25
       <RadixTooltip.Trigger asChild={true}>
25
       <RadixTooltip.Trigger asChild={true}>
26
         <span>{children}</span>
26
         <span>{children}</span>
27
       </RadixTooltip.Trigger>
27
       </RadixTooltip.Trigger>
28
-      <RadixTooltip.Content className={content()} side={side} sideOffset={8}>
28
+      <StyledContent side={side} sideOffset={8}>
29
         {label}
29
         {label}
30
         {kbdProp ? <Kbd variant="tooltip">{kbdProp}</Kbd> : null}
30
         {kbdProp ? <Kbd variant="tooltip">{kbdProp}</Kbd> : null}
31
-        <RadixTooltip.Arrow className={arrow()} />
32
-      </RadixTooltip.Content>
31
+        <StyledArrow />
32
+      </StyledContent>
33
     </RadixTooltip.Root>
33
     </RadixTooltip.Root>
34
   )
34
   )
35
 }
35
 }
36
 
36
 
37
-const button = css({
38
-  border: 'none',
39
-  background: 'none',
40
-  padding: 0,
41
-})
42
-
43
-const content = css({
37
+const StyledContent = styled(RadixTooltip.Content, {
44
   borderRadius: 3,
38
   borderRadius: 3,
45
   padding: '$3 $3 $3 $3',
39
   padding: '$3 $3 $3 $3',
46
   fontSize: '$1',
40
   fontSize: '$1',
53
   userSelect: 'none',
47
   userSelect: 'none',
54
 })
48
 })
55
 
49
 
56
-const arrow = css({
50
+const StyledArrow = styled(RadixTooltip.Arrow, {
57
   fill: '$tooltipBg',
51
   fill: '$tooltipBg',
58
   margin: '0 8px',
52
   margin: '0 8px',
59
 })
53
 })

+ 1
- 0
packages/tldraw/src/components/Tooltip/index.ts View File

1
+export * from './Tooltip'

+ 43
- 0
packages/tldraw/src/components/TopPanel/ColorMenu.tsx View File

1
+import * as React from 'react'
2
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
3
+import { strokes } from '~shape-utils'
4
+import { useTheme, useTLDrawContext } from '~hooks'
5
+import type { Data, ColorStyle } from '~types'
6
+import CircleIcon from '~components/icons/CircleIcon'
7
+import { DMContent, DMRadioItem, DMTriggerIcon } from '~components/DropdownMenu'
8
+import { BoxIcon } from '~components/icons'
9
+import { IconButton } from '~components/IconButton'
10
+import { ToolButton } from '~components/ToolButton'
11
+import { Tooltip } from '~components/Tooltip'
12
+
13
+const selectColor = (s: Data) => s.appState.selectedStyle.color
14
+
15
+export const ColorMenu = React.memo((): JSX.Element => {
16
+  const { theme } = useTheme()
17
+  const { tlstate, useSelector } = useTLDrawContext()
18
+
19
+  const color = useSelector(selectColor)
20
+
21
+  return (
22
+    <DropdownMenu.Root dir="ltr">
23
+      <DMTriggerIcon>
24
+        <CircleIcon size={16} fill={strokes[theme][color]} stroke={strokes[theme][color]} />
25
+      </DMTriggerIcon>
26
+      <DMContent variant="grid">
27
+        {Object.keys(strokes[theme]).map((colorStyle: string) => (
28
+          <ToolButton
29
+            key={colorStyle}
30
+            variant="icon"
31
+            isActive={color === colorStyle}
32
+            onSelect={() => tlstate.style({ color: colorStyle as ColorStyle })}
33
+          >
34
+            <BoxIcon
35
+              fill={strokes[theme][colorStyle as ColorStyle]}
36
+              stroke={strokes[theme][colorStyle as ColorStyle]}
37
+            />
38
+          </ToolButton>
39
+        ))}
40
+      </DMContent>
41
+    </DropdownMenu.Root>
42
+  )
43
+})

+ 100
- 0
packages/tldraw/src/components/TopPanel/DashMenu.tsx View File

1
+import * as React from 'react'
2
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
3
+import { useTLDrawContext } from '~hooks'
4
+import { DashStyle, Data } from '~types'
5
+import { DMContent, DMRadioItem, DMTriggerIcon } from '~components/DropdownMenu'
6
+import { ToolButton } from '~components/ToolButton'
7
+import { Tooltip } from '~components/Tooltip'
8
+
9
+const dashes = {
10
+  [DashStyle.Draw]: <DashDrawIcon />,
11
+  [DashStyle.Solid]: <DashSolidIcon />,
12
+  [DashStyle.Dashed]: <DashDashedIcon />,
13
+  [DashStyle.Dotted]: <DashDottedIcon />,
14
+}
15
+
16
+const selectDash = (s: Data) => s.appState.selectedStyle.dash
17
+
18
+export const DashMenu = React.memo((): JSX.Element => {
19
+  const { tlstate, useSelector } = useTLDrawContext()
20
+
21
+  const dash = useSelector(selectDash)
22
+
23
+  return (
24
+    <DropdownMenu.Root dir="ltr">
25
+      <DMTriggerIcon>{dashes[dash]}</DMTriggerIcon>
26
+      <DMContent>
27
+        {Object.keys(DashStyle).map((dashStyle) => (
28
+          <ToolButton
29
+            key={dashStyle}
30
+            variant="icon"
31
+            isActive={dash === dashStyle}
32
+            onSelect={() => tlstate.style({ dash: dashStyle as DashStyle })}
33
+          >
34
+            {dashes[dashStyle as DashStyle]}
35
+          </ToolButton>
36
+        ))}
37
+      </DMContent>
38
+    </DropdownMenu.Root>
39
+  )
40
+})
41
+
42
+function DashSolidIcon(): JSX.Element {
43
+  return (
44
+    <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
45
+      <circle cx={12} cy={12} r={8} fill="none" strokeWidth={2} strokeLinecap="round" />
46
+    </svg>
47
+  )
48
+}
49
+
50
+function DashDashedIcon(): JSX.Element {
51
+  return (
52
+    <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
53
+      <circle
54
+        cx={12}
55
+        cy={12}
56
+        r={8}
57
+        fill="none"
58
+        strokeWidth={2.5}
59
+        strokeLinecap="round"
60
+        strokeDasharray={50.26548 * 0.1}
61
+      />
62
+    </svg>
63
+  )
64
+}
65
+
66
+const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
67
+
68
+function DashDottedIcon(): JSX.Element {
69
+  return (
70
+    <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
71
+      <circle
72
+        cx={12}
73
+        cy={12}
74
+        r={8}
75
+        fill="none"
76
+        strokeWidth={2.5}
77
+        strokeLinecap="round"
78
+        strokeDasharray={dottedDasharray}
79
+      />
80
+    </svg>
81
+  )
82
+}
83
+
84
+function DashDrawIcon(): JSX.Element {
85
+  return (
86
+    <svg
87
+      width="24"
88
+      height="24"
89
+      viewBox="1 1.5 21 22"
90
+      fill="currentColor"
91
+      stroke="currentColor"
92
+      xmlns="http://www.w3.org/2000/svg"
93
+    >
94
+      <path
95
+        d="M10.0162 19.2768C10.0162 19.2768 9.90679 19.2517 9.6879 19.2017C9.46275 19.1454 9.12816 19.0422 8.68413 18.8921C8.23384 18.7358 7.81482 18.545 7.42707 18.3199C7.03307 18.101 6.62343 17.7883 6.19816 17.3818C5.77289 16.9753 5.33511 16.3718 4.88482 15.5713C4.43453 14.7645 4.1531 13.8545 4.04053 12.8414C3.92795 11.822 4.04991 10.8464 4.40639 9.91451C4.76286 8.98266 5.39452 8.10084 6.30135 7.26906C7.21444 6.44353 8.29325 5.83377 9.5378 5.43976C10.7823 5.05202 11.833 4.92068 12.6898 5.04576C13.5466 5.16459 14.3878 5.43664 15.2133 5.86191C16.0388 6.28718 16.7768 6.8688 17.4272 7.60678C18.0714 8.34475 18.5404 9.21406 18.8344 10.2147C19.1283 11.2153 19.1721 12.2598 18.9657 13.348C18.7593 14.4299 18.2872 15.4337 17.5492 16.3593C16.8112 17.2849 15.9263 18.0072 14.8944 18.5263C13.8624 19.0391 12.9056 19.3174 12.0238 19.3612C11.142 19.405 10.2101 19.2705 9.22823 18.9578C8.24635 18.6451 7.35828 18.151 6.56402 17.4756C5.77601 16.8002 6.08871 16.8658 7.50212 17.6726C8.90927 18.4731 10.1444 18.8484 11.2076 18.7983C12.2645 18.7545 13.2965 18.4825 14.3034 17.9822C15.3102 17.4819 16.1264 16.8221 16.7518 16.0028C17.3772 15.1835 17.7681 14.3111 17.9244 13.3855C18.0808 12.4599 18.0401 11.5781 17.8025 10.74C17.5586 9.902 17.1739 9.15464 16.6486 8.49797C16.1233 7.8413 15.2289 7.27844 13.9656 6.80939C12.7086 6.34034 11.4203 6.20901 10.1007 6.41539C8.78732 6.61552 7.69599 7.06893 6.82669 7.77564C5.96363 8.48859 5.34761 9.26409 4.97863 10.1021C4.60964 10.9402 4.45329 11.8376 4.50958 12.7945C4.56586 13.7513 4.79101 14.6238 5.18501 15.4118C5.57276 16.1998 5.96363 16.8002 6.35764 17.2129C6.75164 17.6257 7.13313 17.9509 7.50212 18.1886C7.87736 18.4325 8.28074 18.642 8.71227 18.8171C9.15005 18.9922 9.47839 19.111 9.69728 19.1736C9.91617 19.2361 10.0256 19.2705 10.0256 19.2768H10.0162Z"
96
+        strokeWidth="2"
97
+      />
98
+    </svg>
99
+  )
100
+}

+ 31
- 0
packages/tldraw/src/components/TopPanel/FillCheckbox.tsx View File

1
+import * as React from 'react'
2
+import * as Checkbox from '@radix-ui/react-checkbox'
3
+import { useTLDrawContext } from '~hooks'
4
+import type { Data } from '~types'
5
+import { Tooltip } from '~components/Tooltip'
6
+import { BoxIcon, IsFilledIcon } from '~components/icons'
7
+import { ToolButton } from '~components/ToolButton'
8
+
9
+const isFilledSelector = (s: Data) => s.appState.selectedStyle.isFilled
10
+
11
+export const FillCheckbox = React.memo((): JSX.Element => {
12
+  const { tlstate, useSelector } = useTLDrawContext()
13
+
14
+  const isFilled = useSelector(isFilledSelector)
15
+
16
+  const handleIsFilledChange = React.useCallback(
17
+    (isFilled: boolean) => tlstate.style({ isFilled }),
18
+    [tlstate]
19
+  )
20
+
21
+  return (
22
+    <Checkbox.Root dir="ltr" asChild checked={isFilled} onCheckedChange={handleIsFilledChange}>
23
+      <ToolButton variant="icon">
24
+        <BoxIcon />
25
+        <Checkbox.Indicator>
26
+          <IsFilledIcon />
27
+        </Checkbox.Indicator>
28
+      </ToolButton>
29
+    </Checkbox.Root>
30
+  )
31
+})

+ 111
- 0
packages/tldraw/src/components/TopPanel/Menu.tsx View File

1
+import * as React from 'react'
2
+import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
3
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
4
+import { useTLDrawContext } from '~hooks'
5
+import { PreferencesMenu } from './PreferencesMenu'
6
+import { DMItem, DMContent, DMDivider, DMSubMenu, DMTriggerIcon } from '~components/DropdownMenu'
7
+import { SmallIcon } from '~components/SmallIcon'
8
+
9
+export const Menu = React.memo(() => {
10
+  const { tlstate } = useTLDrawContext()
11
+
12
+  const handleNew = React.useCallback(() => {
13
+    if (window.confirm('Are you sure you want to start a new project?')) {
14
+      tlstate.newProject()
15
+    }
16
+  }, [tlstate])
17
+
18
+  const handleSave = React.useCallback(() => {
19
+    tlstate.saveProject()
20
+  }, [tlstate])
21
+
22
+  const handleLoad = React.useCallback(() => {
23
+    tlstate.loadProject()
24
+  }, [tlstate])
25
+
26
+  const handleSignOut = React.useCallback(() => {
27
+    tlstate.signOut()
28
+  }, [tlstate])
29
+
30
+  const handleCopy = React.useCallback(() => {
31
+    tlstate.copy()
32
+  }, [tlstate])
33
+
34
+  const handlePaste = React.useCallback(() => {
35
+    tlstate.paste()
36
+  }, [tlstate])
37
+
38
+  const handleCopySvg = React.useCallback(() => {
39
+    tlstate.copySvg()
40
+  }, [tlstate])
41
+
42
+  const handleCopyJson = React.useCallback(() => {
43
+    tlstate.copyJson()
44
+  }, [tlstate])
45
+
46
+  const handleSelectAll = React.useCallback(() => {
47
+    tlstate.selectAll()
48
+  }, [tlstate])
49
+
50
+  const handleDeselectAll = React.useCallback(() => {
51
+    tlstate.deselectAll()
52
+  }, [tlstate])
53
+
54
+  return (
55
+    <DropdownMenu.Root>
56
+      <DMTriggerIcon>
57
+        <HamburgerMenuIcon />
58
+      </DMTriggerIcon>
59
+      <DMContent variant="menu">
60
+        <DMSubMenu label="File...">
61
+          <DMItem onSelect={handleNew} kbd="#N">
62
+            New Project
63
+          </DMItem>
64
+          <DMItem disabled onSelect={handleLoad} kbd="#L">
65
+            Open...
66
+          </DMItem>
67
+          <DMItem disabled onSelect={handleSave} kbd="#S">
68
+            Save
69
+          </DMItem>
70
+          <DMItem disabled onSelect={handleSave} kbd="⇧#S">
71
+            Save As...
72
+          </DMItem>
73
+        </DMSubMenu>
74
+        <DMSubMenu label="Edit...">
75
+          <DMItem onSelect={tlstate.undo} kbd="#Z">
76
+            Undo
77
+          </DMItem>
78
+          <DMItem onSelect={tlstate.redo} kbd="#⇧Z">
79
+            Redo
80
+          </DMItem>
81
+          <DMDivider dir="ltr" />
82
+          <DMItem onSelect={handleCopy} kbd="#C">
83
+            Copy
84
+          </DMItem>
85
+          <DMItem onSelect={handlePaste} kbd="#V">
86
+            Paste
87
+          </DMItem>
88
+          <DMDivider dir="ltr" />
89
+          <DMItem onSelect={handleCopySvg} kbd="#⇧C">
90
+            Copy as SVG
91
+          </DMItem>
92
+          <DMItem onSelect={handleCopyJson}>Copy as JSON</DMItem>
93
+          <DMDivider dir="ltr" />
94
+          <DMItem onSelect={handleSelectAll} kbd="#A">
95
+            Select All
96
+          </DMItem>
97
+          <DMItem onSelect={handleDeselectAll}>Select None</DMItem>
98
+        </DMSubMenu>
99
+        <DMDivider dir="ltr" />
100
+        <PreferencesMenu />
101
+        <DMDivider dir="ltr" />
102
+        <DMItem disabled onSelect={handleSignOut}>
103
+          Sign Out
104
+          <SmallIcon>
105
+            <ExitIcon />
106
+          </SmallIcon>
107
+        </DMItem>
108
+      </DMContent>
109
+    </DropdownMenu.Root>
110
+  )
111
+})

packages/tldraw/src/components/page-panel/page-panel.tsx → packages/tldraw/src/components/TopPanel/PageMenu.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
3
 import { PlusIcon, CheckIcon } from '@radix-ui/react-icons'
3
 import { PlusIcon, CheckIcon } from '@radix-ui/react-icons'
4
-import {
5
-  breakpoints,
6
-  DropdownMenuButton,
7
-  DropdownMenuDivider,
8
-  rowButton,
9
-  menuContent,
10
-  floatingContainer,
11
-  iconWrapper,
12
-} from '~components/shared'
13
-import { PageOptionsDialog } from '~components/page-options-dialog'
14
-import css from '~styles'
4
+import { PageOptionsDialog } from './PageOptionsDialog'
5
+import styled from '~styles'
15
 import { useTLDrawContext } from '~hooks'
6
 import { useTLDrawContext } from '~hooks'
16
 import type { Data } from '~types'
7
 import type { Data } from '~types'
8
+import { DMContent, DMDivider } from '~components/DropdownMenu'
9
+import { SmallIcon } from '~components/SmallIcon'
10
+import { RowButton } from '~components/RowButton'
11
+import { ToolButton } from '~components/ToolButton'
17
 
12
 
18
 const sortedSelector = (s: Data) =>
13
 const sortedSelector = (s: Data) =>
19
   Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
14
   Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
22
 
17
 
23
 const currentPageIdSelector = (s: Data) => s.document.pages[s.appState.currentPageId].id
18
 const currentPageIdSelector = (s: Data) => s.document.pages[s.appState.currentPageId].id
24
 
19
 
25
-export function PagePanel(): JSX.Element {
20
+export function PageMenu(): JSX.Element {
26
   const { useSelector } = useTLDrawContext()
21
   const { useSelector } = useTLDrawContext()
27
 
22
 
28
   const rIsOpen = React.useRef(false)
23
   const rIsOpen = React.useRef(false)
51
 
46
 
52
   return (
47
   return (
53
     <DropdownMenu.Root dir="ltr" open={isOpen} onOpenChange={handleOpenChange}>
48
     <DropdownMenu.Root dir="ltr" open={isOpen} onOpenChange={handleOpenChange}>
54
-      <div className={floatingContainer()}>
55
-        <DropdownMenu.Trigger className={rowButton({ bp: breakpoints, variant: 'noIcon' })}>
56
-          <span>{currentPageName || 'Page'}</span>
57
-        </DropdownMenu.Trigger>
58
-      </div>
59
-      <DropdownMenu.Content className={menuContent()} sideOffset={8} align="start">
49
+      <DropdownMenu.Trigger dir="ltr" asChild>
50
+        <ToolButton variant="text">{currentPageName || 'Page'}</ToolButton>
51
+      </DropdownMenu.Trigger>
52
+      <DMContent variant="menu" align="start">
60
         {isOpen && <PageMenuContent onClose={handleClose} />}
53
         {isOpen && <PageMenuContent onClose={handleClose} />}
61
-      </DropdownMenu.Content>
54
+      </DMContent>
62
     </DropdownMenu.Root>
55
     </DropdownMenu.Root>
63
   )
56
   )
64
 }
57
 }
86
     <>
79
     <>
87
       <DropdownMenu.RadioGroup value={currentPageId} onValueChange={handleChangePage}>
80
       <DropdownMenu.RadioGroup value={currentPageId} onValueChange={handleChangePage}>
88
         {sortedPages.map((page) => (
81
         {sortedPages.map((page) => (
89
-          <div className={buttonWithOptions()} key={page.id}>
82
+          <ButtonWithOptions key={page.id}>
90
             <DropdownMenu.RadioItem
83
             <DropdownMenu.RadioItem
91
-              className={rowButton({ bp: breakpoints, variant: 'pageButton' })}
84
+              title={page.name || 'Page'}
92
               value={page.id}
85
               value={page.id}
86
+              key={page.id}
87
+              asChild
93
             >
88
             >
94
-              <span>{page.name || 'Page'}</span>
95
-              <DropdownMenu.ItemIndicator>
96
-                <div className={iconWrapper({ size: 'small' })}>
97
-                  <CheckIcon />
98
-                </div>
99
-              </DropdownMenu.ItemIndicator>
89
+              <PageButton>
90
+                <span>{page.name || 'Page'}</span>
91
+                <DropdownMenu.ItemIndicator>
92
+                  <SmallIcon>
93
+                    <CheckIcon />
94
+                  </SmallIcon>
95
+                </DropdownMenu.ItemIndicator>
96
+              </PageButton>
100
             </DropdownMenu.RadioItem>
97
             </DropdownMenu.RadioItem>
101
             <PageOptionsDialog page={page} onClose={onClose} />
98
             <PageOptionsDialog page={page} onClose={onClose} />
102
-          </div>
99
+          </ButtonWithOptions>
103
         ))}
100
         ))}
104
       </DropdownMenu.RadioGroup>
101
       </DropdownMenu.RadioGroup>
105
-      <DropdownMenuDivider />
106
-      <DropdownMenuButton onSelect={handleCreatePage}>
107
-        <span>Create Page</span>
108
-        <div className={iconWrapper({ size: 'small' })}>
109
-          <PlusIcon />
110
-        </div>
111
-      </DropdownMenuButton>
102
+      <DMDivider />
103
+      <DropdownMenu.Item onSelect={handleCreatePage} asChild>
104
+        <RowButton>
105
+          <span>Create Page</span>
106
+          <SmallIcon>
107
+            <PlusIcon />
108
+          </SmallIcon>
109
+        </RowButton>
110
+      </DropdownMenu.Item>
112
     </>
111
     </>
113
   )
112
   )
114
 }
113
 }
115
 
114
 
116
-const buttonWithOptions = css({
115
+const ButtonWithOptions = styled('div', {
117
   display: 'grid',
116
   display: 'grid',
118
   gridTemplateColumns: '1fr auto',
117
   gridTemplateColumns: '1fr auto',
119
   gridAutoFlow: 'column',
118
   gridAutoFlow: 'column',
126
     opacity: 1,
125
     opacity: 1,
127
   },
126
   },
128
 })
127
 })
128
+
129
+export const PageButton = styled(RowButton, {
130
+  minWidth: 128,
131
+})

+ 139
- 0
packages/tldraw/src/components/TopPanel/PageOptionsDialog.tsx View File

1
+import * as React from 'react'
2
+import * as Dialog from '@radix-ui/react-alert-dialog'
3
+import { MixerVerticalIcon } from '@radix-ui/react-icons'
4
+import type { Data, TLDrawPage } from '~types'
5
+import { useTLDrawContext } from '~hooks'
6
+import { RowButton, RowButtonProps } from '~components/RowButton'
7
+import styled from '~styles'
8
+import { Divider } from '~components/Divider'
9
+import { IconButton } from '~components/IconButton/IconButton'
10
+import { SmallIcon } from '~components/SmallIcon'
11
+import { breakpoints } from '~components/breakpoints'
12
+
13
+const canDeleteSelector = (s: Data) => {
14
+  return Object.keys(s.document.pages).length > 1
15
+}
16
+
17
+interface PageOptionsDialogProps {
18
+  page: TLDrawPage
19
+  onOpen?: () => void
20
+  onClose?: () => void
21
+}
22
+
23
+export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
24
+  const { tlstate, useSelector } = useTLDrawContext()
25
+
26
+  const [isOpen, setIsOpen] = React.useState(false)
27
+
28
+  const canDelete = useSelector(canDeleteSelector)
29
+
30
+  const rInput = React.useRef<HTMLInputElement>(null)
31
+
32
+  const handleDuplicate = React.useCallback(() => {
33
+    tlstate.duplicatePage(page.id)
34
+    onClose?.()
35
+  }, [tlstate])
36
+
37
+  const handleDelete = React.useCallback(() => {
38
+    if (window.confirm(`Are you sure you want to delete this page?`)) {
39
+      tlstate.deletePage(page.id)
40
+      onClose?.()
41
+    }
42
+  }, [tlstate])
43
+
44
+  const handleOpenChange = React.useCallback(
45
+    (isOpen: boolean) => {
46
+      setIsOpen(isOpen)
47
+
48
+      if (isOpen) {
49
+        onOpen?.()
50
+        return
51
+      }
52
+    },
53
+    [tlstate, name]
54
+  )
55
+
56
+  function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
57
+    e.stopPropagation()
58
+  }
59
+
60
+  // TODO: Replace with text input
61
+  function handleRename() {
62
+    const nextName = window.prompt('New name:', page.name)
63
+    tlstate.renamePage(page.id, nextName || page.name || 'Page')
64
+  }
65
+
66
+  React.useEffect(() => {
67
+    if (isOpen) {
68
+      requestAnimationFrame(() => {
69
+        rInput.current?.focus()
70
+        rInput.current?.select()
71
+      })
72
+    }
73
+  }, [isOpen])
74
+
75
+  return (
76
+    <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>
77
+      <Dialog.Trigger asChild data-shy="true">
78
+        <IconButton bp={breakpoints}>
79
+          <SmallIcon>
80
+            <MixerVerticalIcon />
81
+          </SmallIcon>
82
+        </IconButton>
83
+      </Dialog.Trigger>
84
+      <StyledDialogOverlay />
85
+      <StyledDialogContent onKeyDown={stopPropagation} onKeyUp={stopPropagation}>
86
+        <DialogAction onSelect={handleRename}>Rename</DialogAction>
87
+        <DialogAction onSelect={handleDuplicate}>Duplicate</DialogAction>
88
+        <DialogAction disabled={!canDelete} onSelect={handleDelete}>
89
+          Delete
90
+        </DialogAction>
91
+        <Divider />
92
+        <Dialog.Cancel asChild>
93
+          <RowButton>Cancel</RowButton>
94
+        </Dialog.Cancel>
95
+      </StyledDialogContent>
96
+    </Dialog.Root>
97
+  )
98
+}
99
+
100
+/* -------------------------------------------------- */
101
+/*                       Dialog                       */
102
+/* -------------------------------------------------- */
103
+
104
+export const StyledDialogContent = styled(Dialog.Content, {
105
+  position: 'fixed',
106
+  top: '50%',
107
+  left: '50%',
108
+  transform: 'translate(-50%, -50%)',
109
+  minWidth: 240,
110
+  maxWidth: 'fit-content',
111
+  maxHeight: '85vh',
112
+  marginTop: '-5vh',
113
+  pointerEvents: 'all',
114
+  backgroundColor: '$panel',
115
+  border: '1px solid $panelBorder',
116
+  padding: '$0',
117
+  borderRadius: '$2',
118
+  font: '$ui',
119
+  '&:focus': {
120
+    outline: 'none',
121
+  },
122
+})
123
+
124
+export const StyledDialogOverlay = styled(Dialog.Overlay, {
125
+  backgroundColor: 'rgba(0, 0, 0, .15)',
126
+  position: 'fixed',
127
+  top: 0,
128
+  right: 0,
129
+  bottom: 0,
130
+  left: 0,
131
+})
132
+
133
+function DialogAction({ onSelect, ...rest }: RowButtonProps) {
134
+  return (
135
+    <Dialog.Action asChild onClick={onSelect}>
136
+      <RowButton {...rest} />
137
+    </Dialog.Action>
138
+  )
139
+}

+ 70
- 0
packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx View File

1
+import * as React from 'react'
2
+import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/DropdownMenu'
3
+import { useTLDrawContext } from '~hooks'
4
+import type { Data } from '~types'
5
+
6
+const settingsSelector = (s: Data) => s.settings
7
+
8
+export function PreferencesMenu() {
9
+  const { tlstate, useSelector } = useTLDrawContext()
10
+
11
+  const settings = useSelector(settingsSelector)
12
+
13
+  const toggleDebugMode = React.useCallback(() => {
14
+    tlstate.setSetting('isDebugMode', (v) => !v)
15
+  }, [tlstate])
16
+
17
+  const toggleDarkMode = React.useCallback(() => {
18
+    tlstate.setSetting('isDarkMode', (v) => !v)
19
+  }, [tlstate])
20
+
21
+  const toggleFocusMode = React.useCallback(() => {
22
+    tlstate.setSetting('isFocusMode', (v) => !v)
23
+  }, [tlstate])
24
+
25
+  const toggleRotateHandle = React.useCallback(() => {
26
+    tlstate.setSetting('showRotateHandles', (v) => !v)
27
+  }, [tlstate])
28
+
29
+  const toggleBoundShapesHandle = React.useCallback(() => {
30
+    tlstate.setSetting('showBindingHandles', (v) => !v)
31
+  }, [tlstate])
32
+
33
+  const toggleisSnapping = React.useCallback(() => {
34
+    tlstate.setSetting('isSnapping', (v) => !v)
35
+  }, [tlstate])
36
+
37
+  const toggleCloneControls = React.useCallback(() => {
38
+    tlstate.setSetting('showCloneHandles', (v) => !v)
39
+  }, [tlstate])
40
+
41
+  return (
42
+    <DMSubMenu label="Preferences">
43
+      <DMCheckboxItem checked={settings.isDarkMode} onCheckedChange={toggleDarkMode} kbd="#⇧D">
44
+        Dark Mode
45
+      </DMCheckboxItem>
46
+      <DMCheckboxItem checked={settings.isFocusMode} onCheckedChange={toggleFocusMode} kbd="⇧.">
47
+        Focus Mode
48
+      </DMCheckboxItem>
49
+      <DMCheckboxItem checked={settings.isDebugMode} onCheckedChange={toggleDebugMode}>
50
+        Debug Mode
51
+      </DMCheckboxItem>
52
+      <DMDivider />
53
+      <DMCheckboxItem checked={settings.showRotateHandles} onCheckedChange={toggleRotateHandle}>
54
+        Rotate Handles
55
+      </DMCheckboxItem>
56
+      <DMCheckboxItem
57
+        checked={settings.showBindingHandles}
58
+        onCheckedChange={toggleBoundShapesHandle}
59
+      >
60
+        Binding Handles
61
+      </DMCheckboxItem>
62
+      <DMCheckboxItem checked={settings.showCloneHandles} onCheckedChange={toggleCloneControls}>
63
+        Clone Handles
64
+      </DMCheckboxItem>
65
+      <DMCheckboxItem checked={settings.isSnapping} onCheckedChange={toggleisSnapping}>
66
+        Always Show Snaps
67
+      </DMCheckboxItem>
68
+    </DMSubMenu>
69
+  )
70
+}

+ 39
- 0
packages/tldraw/src/components/TopPanel/SizeMenu.tsx View File

1
+import * as React from 'react'
2
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
3
+import { Data, SizeStyle } from '~types'
4
+import { useTLDrawContext } from '~hooks'
5
+import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
6
+import { ToolButton } from '~components/ToolButton'
7
+import { SizeSmallIcon, SizeMediumIcon, SizeLargeIcon } from '~components/icons'
8
+import { Tooltip } from '~components/Tooltip'
9
+
10
+const sizes = {
11
+  [SizeStyle.Small]: <SizeSmallIcon />,
12
+  [SizeStyle.Medium]: <SizeMediumIcon />,
13
+  [SizeStyle.Large]: <SizeLargeIcon />,
14
+}
15
+
16
+const selectSize = (s: Data) => s.appState.selectedStyle.size
17
+
18
+export const SizeMenu = React.memo((): JSX.Element => {
19
+  const { tlstate, useSelector } = useTLDrawContext()
20
+
21
+  const size = useSelector(selectSize)
22
+
23
+  return (
24
+    <DropdownMenu.Root dir="ltr">
25
+      <DMTriggerIcon>{sizes[size as SizeStyle]}</DMTriggerIcon>
26
+      <DMContent>
27
+        {Object.keys(SizeStyle).map((sizeStyle: string) => (
28
+          <ToolButton
29
+            key={sizeStyle}
30
+            isActive={size === sizeStyle}
31
+            onSelect={() => tlstate.style({ size: sizeStyle as SizeStyle })}
32
+          >
33
+            {sizes[sizeStyle as SizeStyle]}
34
+          </ToolButton>
35
+        ))}
36
+      </DMContent>
37
+    </DropdownMenu.Root>
38
+  )
39
+})

+ 58
- 0
packages/tldraw/src/components/TopPanel/TopPanel.tsx View File

1
+import * as React from 'react'
2
+import { Menu } from './Menu'
3
+import styled from '~styles'
4
+import { PageMenu } from './PageMenu'
5
+import { ZoomMenu } from './ZoomMenu'
6
+import { DashMenu } from './DashMenu'
7
+import { SizeMenu } from './SizeMenu'
8
+import { FillCheckbox } from './FillCheckbox'
9
+import { ColorMenu } from './ColorMenu'
10
+import { Panel } from '~components/Panel'
11
+
12
+interface TopPanelProps {
13
+  showPages: boolean
14
+  showMenu: boolean
15
+  showStyles: boolean
16
+  showZoom: boolean
17
+}
18
+
19
+export function TopPanel({ showPages, showMenu, showStyles, showZoom }: TopPanelProps) {
20
+  return (
21
+    <StyledTopPanel>
22
+      {(showMenu || showPages) && (
23
+        <Panel side="left">
24
+          {showMenu && <Menu />}
25
+          {showPages && <PageMenu />}
26
+        </Panel>
27
+      )}
28
+      <StyledSpacer />
29
+      {(showStyles || showZoom) && (
30
+        <Panel side="right">
31
+          {showStyles && (
32
+            <>
33
+              <ColorMenu />
34
+              <SizeMenu />
35
+              <DashMenu />
36
+              <FillCheckbox />
37
+            </>
38
+          )}
39
+          {showZoom && <ZoomMenu />}
40
+        </Panel>
41
+      )}
42
+    </StyledTopPanel>
43
+  )
44
+}
45
+
46
+const StyledTopPanel = styled('div', {
47
+  width: '100%',
48
+  position: 'absolute',
49
+  top: 0,
50
+  left: 0,
51
+  right: 0,
52
+  display: 'flex',
53
+  flexDirection: 'row',
54
+})
55
+
56
+const StyledSpacer = styled('div', {
57
+  flexGrow: 2,
58
+})

+ 43
- 0
packages/tldraw/src/components/TopPanel/ZoomMenu.tsx View File

1
+import * as React from 'react'
2
+import { useTLDrawContext } from '~hooks'
3
+import type { Data } from '~types'
4
+import styled from '~styles'
5
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
6
+import { DMItem, DMContent } from '~components/DropdownMenu'
7
+import { ToolButton } from '~components/ToolButton'
8
+
9
+const zoomSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId].camera.zoom
10
+
11
+export function ZoomMenu() {
12
+  const { tlstate, useSelector } = useTLDrawContext()
13
+  const zoom = useSelector(zoomSelector)
14
+
15
+  return (
16
+    <DropdownMenu.Root>
17
+      <DropdownMenu.Trigger asChild>
18
+        <FixedWidthToolButton variant="text">{Math.round(zoom * 100)}%</FixedWidthToolButton>
19
+      </DropdownMenu.Trigger>
20
+      <DMContent align="end">
21
+        <DMItem onSelect={tlstate.zoomIn} kbd="#+">
22
+          Zoom In
23
+        </DMItem>
24
+        <DMItem onSelect={tlstate.zoomOut} kbd="#−">
25
+          Zoom Out
26
+        </DMItem>
27
+        <DMItem onSelect={tlstate.zoomToActual} kbd="⇧0">
28
+          To 100%
29
+        </DMItem>
30
+        <DMItem onSelect={tlstate.zoomToFit} kbd="⇧1">
31
+          To Fit
32
+        </DMItem>
33
+        <DMItem onSelect={tlstate.zoomToSelection} kbd="⇧2">
34
+          To Selection
35
+        </DMItem>
36
+      </DMContent>
37
+    </DropdownMenu.Root>
38
+  )
39
+}
40
+
41
+const FixedWidthToolButton = styled(ToolButton, {
42
+  minWidth: 56,
43
+})

+ 1
- 0
packages/tldraw/src/components/TopPanel/index.ts View File

1
+export * from './TopPanel'

packages/tldraw/src/components/shared/breakpoints.tsx → packages/tldraw/src/components/breakpoints.tsx View File


+ 0
- 381
packages/tldraw/src/components/context-menu/context-menu.tsx View File

1
-import * as React from 'react'
2
-import css from '~styles'
3
-import * as RadixContextMenu from '@radix-ui/react-context-menu'
4
-import { useTLDrawContext } from '~hooks'
5
-import { Data, AlignType, DistributeType, StretchType } from '~types'
6
-import {
7
-  Kbd,
8
-  iconWrapper,
9
-  breakpoints,
10
-  rowButton,
11
-  ContextMenuArrow,
12
-  ContextMenuDivider,
13
-  ContextMenuButton,
14
-  ContextMenuSubMenu,
15
-  ContextMenuIconButton,
16
-  ContextMenuRoot,
17
-  menuContent,
18
-} from '../shared'
19
-import {
20
-  ChevronRightIcon,
21
-  AlignBottomIcon,
22
-  AlignCenterHorizontallyIcon,
23
-  AlignCenterVerticallyIcon,
24
-  AlignLeftIcon,
25
-  AlignRightIcon,
26
-  AlignTopIcon,
27
-  SpaceEvenlyHorizontallyIcon,
28
-  SpaceEvenlyVerticallyIcon,
29
-  StretchHorizontallyIcon,
30
-  StretchVerticallyIcon,
31
-} from '@radix-ui/react-icons'
32
-
33
-const has1SelectedIdsSelector = (s: Data) => {
34
-  return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
35
-}
36
-const has2SelectedIdsSelector = (s: Data) => {
37
-  return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1
38
-}
39
-const has3SelectedIdsSelector = (s: Data) => {
40
-  return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
41
-}
42
-
43
-const isDebugModeSelector = (s: Data) => {
44
-  return s.settings.isDebugMode
45
-}
46
-
47
-const hasGroupSelectedSelector = (s: Data) => {
48
-  return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
49
-    (id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined
50
-  )
51
-}
52
-
53
-interface ContextMenuProps {
54
-  children: React.ReactNode
55
-}
56
-
57
-export const ContextMenu = ({ children }: ContextMenuProps): JSX.Element => {
58
-  const { tlstate, useSelector } = useTLDrawContext()
59
-  const hasSelection = useSelector(has1SelectedIdsSelector)
60
-  const hasTwoOrMore = useSelector(has2SelectedIdsSelector)
61
-  const hasThreeOrMore = useSelector(has3SelectedIdsSelector)
62
-  const isDebugMode = useSelector(isDebugModeSelector)
63
-  const hasGroupSelected = useSelector(hasGroupSelectedSelector)
64
-
65
-  const rContent = React.useRef<HTMLDivElement>(null)
66
-
67
-  const handleFlipHorizontal = React.useCallback(() => {
68
-    tlstate.flipHorizontal()
69
-  }, [tlstate])
70
-
71
-  const handleFlipVertical = React.useCallback(() => {
72
-    tlstate.flipVertical()
73
-  }, [tlstate])
74
-
75
-  const handleDuplicate = React.useCallback(() => {
76
-    tlstate.duplicate()
77
-  }, [tlstate])
78
-
79
-  const handleGroup = React.useCallback(() => {
80
-    tlstate.group()
81
-  }, [tlstate])
82
-
83
-  const handleMoveToBack = React.useCallback(() => {
84
-    tlstate.moveToBack()
85
-  }, [tlstate])
86
-
87
-  const handleMoveBackward = React.useCallback(() => {
88
-    tlstate.moveBackward()
89
-  }, [tlstate])
90
-
91
-  const handleMoveForward = React.useCallback(() => {
92
-    tlstate.moveForward()
93
-  }, [tlstate])
94
-
95
-  const handleMoveToFront = React.useCallback(() => {
96
-    tlstate.moveToFront()
97
-  }, [tlstate])
98
-
99
-  const handleDelete = React.useCallback(() => {
100
-    tlstate.delete()
101
-  }, [tlstate])
102
-
103
-  const handleCopyJson = React.useCallback(() => {
104
-    tlstate.copyJson()
105
-  }, [tlstate])
106
-
107
-  const handleCopy = React.useCallback(() => {
108
-    tlstate.copy()
109
-  }, [tlstate])
110
-
111
-  const handlePaste = React.useCallback(() => {
112
-    tlstate.paste()
113
-  }, [tlstate])
114
-
115
-  const handleCopySvg = React.useCallback(() => {
116
-    tlstate.copySvg()
117
-  }, [tlstate])
118
-
119
-  const handleUndo = React.useCallback(() => {
120
-    tlstate.undo()
121
-  }, [tlstate])
122
-
123
-  const handleRedo = React.useCallback(() => {
124
-    tlstate.redo()
125
-  }, [tlstate])
126
-
127
-  return (
128
-    <ContextMenuRoot>
129
-      <RadixContextMenu.Trigger dir="ltr">{children}</RadixContextMenu.Trigger>
130
-      <RadixContextMenu.Content dir="ltr" className={menuContent()} ref={rContent}>
131
-        {hasSelection ? (
132
-          <>
133
-            <ContextMenuButton onSelect={handleFlipHorizontal}>
134
-              <span>Flip Horizontal</span>
135
-              <Kbd variant="menu">⇧H</Kbd>
136
-            </ContextMenuButton>
137
-            <ContextMenuButton onSelect={handleFlipVertical}>
138
-              <span>Flip Vertical</span>
139
-              <Kbd variant="menu">⇧V</Kbd>
140
-            </ContextMenuButton>
141
-            <ContextMenuButton onSelect={handleDuplicate}>
142
-              <span>Duplicate</span>
143
-              <Kbd variant="menu">#D</Kbd>
144
-            </ContextMenuButton>
145
-            <ContextMenuDivider />
146
-            {hasGroupSelected ||
147
-              (hasTwoOrMore && (
148
-                <>
149
-                  {hasGroupSelected && (
150
-                    <ContextMenuButton onSelect={handleGroup}>
151
-                      <span>Ungroup</span>
152
-                      <Kbd variant="menu">#⇧G</Kbd>
153
-                    </ContextMenuButton>
154
-                  )}
155
-                  {hasTwoOrMore && (
156
-                    <ContextMenuButton onSelect={handleGroup}>
157
-                      <span>Group</span>
158
-                      <Kbd variant="menu">#G</Kbd>
159
-                    </ContextMenuButton>
160
-                  )}
161
-                </>
162
-              ))}
163
-            <ContextMenuSubMenu label="Move">
164
-              <ContextMenuButton onSelect={handleMoveToFront}>
165
-                <span>To Front</span>
166
-                <Kbd variant="menu">⇧]</Kbd>
167
-              </ContextMenuButton>
168
-              <ContextMenuButton onSelect={handleMoveForward}>
169
-                <span>Forward</span>
170
-                <Kbd variant="menu">]</Kbd>
171
-              </ContextMenuButton>
172
-              <ContextMenuButton onSelect={handleMoveBackward}>
173
-                <span>Backward</span>
174
-                <Kbd variant="menu">[</Kbd>
175
-              </ContextMenuButton>
176
-              <ContextMenuButton onSelect={handleMoveToBack}>
177
-                <span>To Back</span>
178
-                <Kbd variant="menu">⇧[</Kbd>
179
-              </ContextMenuButton>
180
-            </ContextMenuSubMenu>
181
-            <MoveToPageMenu />
182
-            {hasTwoOrMore && (
183
-              <AlignDistributeSubMenu hasTwoOrMore={hasTwoOrMore} hasThreeOrMore={hasThreeOrMore} />
184
-            )}
185
-            <ContextMenuDivider />
186
-            <ContextMenuButton onSelect={handleCopy}>
187
-              <span>Copy</span>
188
-              <Kbd variant="menu">#C</Kbd>
189
-            </ContextMenuButton>
190
-            <ContextMenuButton onSelect={handleCopySvg}>
191
-              <span>Copy to SVG</span>
192
-              <Kbd variant="menu">⇧#C</Kbd>
193
-            </ContextMenuButton>
194
-            {isDebugMode && (
195
-              <ContextMenuButton onSelect={handleCopyJson}>
196
-                <span>Copy to JSON</span>
197
-              </ContextMenuButton>
198
-            )}
199
-            <ContextMenuButton onSelect={handlePaste}>
200
-              <span>Paste</span>
201
-              <Kbd variant="menu">#V</Kbd>
202
-            </ContextMenuButton>
203
-            <ContextMenuDivider />
204
-            <ContextMenuButton onSelect={handleDelete}>
205
-              <span>Delete</span>
206
-              <Kbd variant="menu">⌫</Kbd>
207
-            </ContextMenuButton>
208
-          </>
209
-        ) : (
210
-          <>
211
-            <ContextMenuButton onSelect={handlePaste}>
212
-              <span>Paste</span>
213
-              <Kbd variant="menu">#V</Kbd>
214
-            </ContextMenuButton>
215
-            <ContextMenuButton onSelect={handleUndo}>
216
-              <span>Undo</span>
217
-              <Kbd variant="menu">#Z</Kbd>
218
-            </ContextMenuButton>
219
-            <ContextMenuButton onSelect={handleRedo}>
220
-              <span>Redo</span>
221
-              <Kbd variant="menu">#⇧Z</Kbd>
222
-            </ContextMenuButton>
223
-          </>
224
-        )}
225
-      </RadixContextMenu.Content>
226
-    </ContextMenuRoot>
227
-  )
228
-}
229
-
230
-function AlignDistributeSubMenu({
231
-  hasThreeOrMore,
232
-}: {
233
-  hasTwoOrMore: boolean
234
-  hasThreeOrMore: boolean
235
-}) {
236
-  const { tlstate } = useTLDrawContext()
237
-
238
-  const alignTop = React.useCallback(() => {
239
-    tlstate.align(AlignType.Top)
240
-  }, [tlstate])
241
-
242
-  const alignCenterVertical = React.useCallback(() => {
243
-    tlstate.align(AlignType.CenterVertical)
244
-  }, [tlstate])
245
-
246
-  const alignBottom = React.useCallback(() => {
247
-    tlstate.align(AlignType.Bottom)
248
-  }, [tlstate])
249
-
250
-  const stretchVertically = React.useCallback(() => {
251
-    tlstate.stretch(StretchType.Vertical)
252
-  }, [tlstate])
253
-
254
-  const distributeVertically = React.useCallback(() => {
255
-    tlstate.distribute(DistributeType.Vertical)
256
-  }, [tlstate])
257
-
258
-  const alignLeft = React.useCallback(() => {
259
-    tlstate.align(AlignType.Left)
260
-  }, [tlstate])
261
-
262
-  const alignCenterHorizontal = React.useCallback(() => {
263
-    tlstate.align(AlignType.CenterHorizontal)
264
-  }, [tlstate])
265
-
266
-  const alignRight = React.useCallback(() => {
267
-    tlstate.align(AlignType.Right)
268
-  }, [tlstate])
269
-
270
-  const stretchHorizontally = React.useCallback(() => {
271
-    tlstate.stretch(StretchType.Horizontal)
272
-  }, [tlstate])
273
-
274
-  const distributeHorizontally = React.useCallback(() => {
275
-    tlstate.distribute(DistributeType.Horizontal)
276
-  }, [tlstate])
277
-
278
-  return (
279
-    <ContextMenuRoot>
280
-      <RadixContextMenu.TriggerItem className={rowButton({ bp: breakpoints })}>
281
-        <span>Align / Distribute</span>
282
-        <div className={iconWrapper({ size: 'small' })}>
283
-          <ChevronRightIcon />
284
-        </div>
285
-      </RadixContextMenu.TriggerItem>
286
-      <RadixContextMenu.Content
287
-        className={grid({ selectedStyle: hasThreeOrMore ? 'threeOrMore' : 'twoOrMore' })}
288
-        sideOffset={2}
289
-        alignOffset={-2}
290
-      >
291
-        <ContextMenuIconButton onSelect={alignLeft}>
292
-          <AlignLeftIcon />
293
-        </ContextMenuIconButton>
294
-        <ContextMenuIconButton onSelect={alignCenterHorizontal}>
295
-          <AlignCenterHorizontallyIcon />
296
-        </ContextMenuIconButton>
297
-        <ContextMenuIconButton onSelect={alignRight}>
298
-          <AlignRightIcon />
299
-        </ContextMenuIconButton>
300
-        <ContextMenuIconButton onSelect={stretchHorizontally}>
301
-          <StretchHorizontallyIcon />
302
-        </ContextMenuIconButton>
303
-        {hasThreeOrMore && (
304
-          <ContextMenuIconButton onSelect={distributeHorizontally}>
305
-            <SpaceEvenlyHorizontallyIcon />
306
-          </ContextMenuIconButton>
307
-        )}
308
-        <ContextMenuIconButton onSelect={alignTop}>
309
-          <AlignTopIcon />
310
-        </ContextMenuIconButton>
311
-        <ContextMenuIconButton onSelect={alignCenterVertical}>
312
-          <AlignCenterVerticallyIcon />
313
-        </ContextMenuIconButton>
314
-        <ContextMenuIconButton onSelect={alignBottom}>
315
-          <AlignBottomIcon />
316
-        </ContextMenuIconButton>
317
-        <ContextMenuIconButton onSelect={stretchVertically}>
318
-          <StretchVerticallyIcon />
319
-        </ContextMenuIconButton>
320
-        {hasThreeOrMore && (
321
-          <ContextMenuIconButton onSelect={distributeVertically}>
322
-            <SpaceEvenlyVerticallyIcon />
323
-          </ContextMenuIconButton>
324
-        )}
325
-        <ContextMenuArrow offset={13} />
326
-      </RadixContextMenu.Content>
327
-    </ContextMenuRoot>
328
-  )
329
-}
330
-
331
-const grid = css(menuContent, {
332
-  display: 'grid',
333
-  variants: {
334
-    selectedStyle: {
335
-      threeOrMore: {
336
-        gridTemplateColumns: 'repeat(5, auto)',
337
-      },
338
-      twoOrMore: {
339
-        gridTemplateColumns: 'repeat(4, auto)',
340
-      },
341
-    },
342
-  },
343
-})
344
-
345
-const currentPageIdSelector = (s: Data) => s.appState.currentPageId
346
-const documentPagesSelector = (s: Data) => s.document.pages
347
-
348
-function MoveToPageMenu(): JSX.Element | null {
349
-  const { tlstate, useSelector } = useTLDrawContext()
350
-  const currentPageId = useSelector(currentPageIdSelector)
351
-  const documentPages = useSelector(documentPagesSelector)
352
-
353
-  const sorted = Object.values(documentPages)
354
-    .sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
355
-    .filter((a) => a.id !== currentPageId)
356
-
357
-  if (sorted.length === 0) return null
358
-
359
-  return (
360
-    <ContextMenuRoot>
361
-      <RadixContextMenu.TriggerItem className={rowButton({ bp: breakpoints })}>
362
-        <span>Move To Page</span>
363
-        <div className={iconWrapper({ size: 'small' })}>
364
-          <ChevronRightIcon />
365
-        </div>
366
-      </RadixContextMenu.TriggerItem>
367
-      <RadixContextMenu.Content className={menuContent()} sideOffset={2} alignOffset={-2}>
368
-        {sorted.map(({ id, name }, i) => (
369
-          <ContextMenuButton
370
-            key={id}
371
-            disabled={id === currentPageId}
372
-            onSelect={() => tlstate.moveToPage(id)}
373
-          >
374
-            <span>{name || `Page ${i}`}</span>
375
-          </ContextMenuButton>
376
-        ))}
377
-        <ContextMenuArrow offset={13} />
378
-      </RadixContextMenu.Content>
379
-    </ContextMenuRoot>
380
-  )
381
-}

+ 0
- 1
packages/tldraw/src/components/context-menu/index.ts View File

1
-export * from './context-menu'

+ 22
- 0
packages/tldraw/src/components/icons/BoxIcon.tsx View File

1
+import * as React from 'react'
2
+
3
+export function BoxIcon({
4
+  fill = 'none',
5
+  stroke = 'currentColor',
6
+}: {
7
+  fill?: string
8
+  stroke?: string
9
+}): JSX.Element {
10
+  return (
11
+    <svg
12
+      width="24"
13
+      height="24"
14
+      viewBox="0 0 24 24"
15
+      stroke={stroke}
16
+      fill={fill}
17
+      xmlns="http://www.w3.org/2000/svg"
18
+    >
19
+      <rect x="4" y="4" width="16" height="16" rx="2" strokeWidth="2" />
20
+    </svg>
21
+  )
22
+}

packages/tldraw/src/components/icons/check.tsx → packages/tldraw/src/components/icons/CheckIcon.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
 
2
 
3
-function SvgCheck(props: React.SVGProps<SVGSVGElement>): JSX.Element {
3
+function CheckIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
   return (
4
   return (
5
     <svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
5
     <svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
6
       <path
6
       <path
17
   )
17
   )
18
 }
18
 }
19
 
19
 
20
-export default SvgCheck
20
+export default CheckIcon

packages/tldraw/src/components/icons/circle.tsx → packages/tldraw/src/components/icons/CircleIcon.tsx View File


+ 17
- 0
packages/tldraw/src/components/icons/DashDashedIcon.tsx View File

1
+import * as React from 'react'
2
+
3
+export function DashDashedIcon(): JSX.Element {
4
+  return (
5
+    <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
6
+      <circle
7
+        cx={12}
8
+        cy={12}
9
+        r={8}
10
+        fill="none"
11
+        strokeWidth={2.5}
12
+        strokeLinecap="round"
13
+        strokeDasharray={50.26548 * 0.1}
14
+      />
15
+    </svg>
16
+  )
17
+}

+ 19
- 0
packages/tldraw/src/components/icons/DashDottedIcon.tsx View File

1
+import * as React from 'react'
2
+
3
+const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
4
+
5
+export function DashDottedIcon(): JSX.Element {
6
+  return (
7
+    <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
8
+      <circle
9
+        cx={12}
10
+        cy={12}
11
+        r={8}
12
+        fill="none"
13
+        strokeWidth={2.5}
14
+        strokeLinecap="round"
15
+        strokeDasharray={dottedDasharray}
16
+      />
17
+    </svg>
18
+  )
19
+}

+ 19
- 0
packages/tldraw/src/components/icons/DashDrawIcon.tsx View File

1
+import * as React from 'react'
2
+
3
+export function DashDrawIcon(): JSX.Element {
4
+  return (
5
+    <svg
6
+      width="24"
7
+      height="24"
8
+      viewBox="1 1.5 21 22"
9
+      fill="currentColor"
10
+      stroke="currentColor"
11
+      xmlns="http://www.w3.org/2000/svg"
12
+    >
13
+      <path
14
+        d="M10.0162 19.2768C10.0162 19.2768 9.90679 19.2517 9.6879 19.2017C9.46275 19.1454 9.12816 19.0422 8.68413 18.8921C8.23384 18.7358 7.81482 18.545 7.42707 18.3199C7.03307 18.101 6.62343 17.7883 6.19816 17.3818C5.77289 16.9753 5.33511 16.3718 4.88482 15.5713C4.43453 14.7645 4.1531 13.8545 4.04053 12.8414C3.92795 11.822 4.04991 10.8464 4.40639 9.91451C4.76286 8.98266 5.39452 8.10084 6.30135 7.26906C7.21444 6.44353 8.29325 5.83377 9.5378 5.43976C10.7823 5.05202 11.833 4.92068 12.6898 5.04576C13.5466 5.16459 14.3878 5.43664 15.2133 5.86191C16.0388 6.28718 16.7768 6.8688 17.4272 7.60678C18.0714 8.34475 18.5404 9.21406 18.8344 10.2147C19.1283 11.2153 19.1721 12.2598 18.9657 13.348C18.7593 14.4299 18.2872 15.4337 17.5492 16.3593C16.8112 17.2849 15.9263 18.0072 14.8944 18.5263C13.8624 19.0391 12.9056 19.3174 12.0238 19.3612C11.142 19.405 10.2101 19.2705 9.22823 18.9578C8.24635 18.6451 7.35828 18.151 6.56402 17.4756C5.77601 16.8002 6.08871 16.8658 7.50212 17.6726C8.90927 18.4731 10.1444 18.8484 11.2076 18.7983C12.2645 18.7545 13.2965 18.4825 14.3034 17.9822C15.3102 17.4819 16.1264 16.8221 16.7518 16.0028C17.3772 15.1835 17.7681 14.3111 17.9244 13.3855C18.0808 12.4599 18.0401 11.5781 17.8025 10.74C17.5586 9.902 17.1739 9.15464 16.6486 8.49797C16.1233 7.8413 15.2289 7.27844 13.9656 6.80939C12.7086 6.34034 11.4203 6.20901 10.1007 6.41539C8.78732 6.61552 7.69599 7.06893 6.82669 7.77564C5.96363 8.48859 5.34761 9.26409 4.97863 10.1021C4.60964 10.9402 4.45329 11.8376 4.50958 12.7945C4.56586 13.7513 4.79101 14.6238 5.18501 15.4118C5.57276 16.1998 5.96363 16.8002 6.35764 17.2129C6.75164 17.6257 7.13313 17.9509 7.50212 18.1886C7.87736 18.4325 8.28074 18.642 8.71227 18.8171C9.15005 18.9922 9.47839 19.111 9.69728 19.1736C9.91617 19.2361 10.0256 19.2705 10.0256 19.2768H10.0162Z"
15
+        strokeWidth="2"
16
+      />
17
+    </svg>
18
+  )
19
+}

+ 9
- 0
packages/tldraw/src/components/icons/DashSolidIcon.tsx View File

1
+import * as React from 'react'
2
+
3
+export function DashSolidIcon(): JSX.Element {
4
+  return (
5
+    <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
6
+      <circle cx={12} cy={12} r={8} fill="none" strokeWidth={2} strokeLinecap="round" />
7
+    </svg>
8
+  )
9
+}

+ 18
- 0
packages/tldraw/src/components/icons/IsFilledIcon.tsx View File

1
+import * as React from 'react'
2
+
3
+export function IsFilledIcon(): JSX.Element {
4
+  return (
5
+    <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
6
+      <rect
7
+        x="4"
8
+        y="4"
9
+        width="16"
10
+        height="16"
11
+        rx="2"
12
+        strokeWidth="2"
13
+        fill="currentColor"
14
+        opacity=".3"
15
+      />
16
+    </svg>
17
+  )
18
+}

packages/tldraw/src/components/icons/redo.tsx → packages/tldraw/src/components/icons/RedoIcon.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
 
2
 
3
-function SvgRedo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
3
+export function RedoIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
   return (
4
   return (
5
-    <svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
5
+    <svg viewBox="0 -1 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
6
       <path
6
       <path
7
         fillRule="evenodd"
7
         fillRule="evenodd"
8
         clipRule="evenodd"
8
         clipRule="evenodd"
16
     </svg>
16
     </svg>
17
   )
17
   )
18
 }
18
 }
19
-
20
-export default SvgRedo

+ 12
- 0
packages/tldraw/src/components/icons/SizeLargeIcon.tsx View File

1
+import * as React from 'react'
2
+
3
+export function SizeLargeIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+  return (
5
+    <svg viewBox="-4 -4 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
6
+      <path
7
+        d="M7.68191 19C7.53525 19 7.46191 18.9267 7.46191 18.78V5H10.1219C10.2686 5 10.3419 5.07333 10.3419 5.22V16.56H13.4419V15.02H15.7619C15.9086 15.02 15.9819 15.0933 15.9819 15.24V19H7.68191Z"
8
+        fill="black"
9
+      />
10
+    </svg>
11
+  )
12
+}

+ 12
- 0
packages/tldraw/src/components/icons/SizeMediumIcon.tsx View File

1
+import * as React from 'react'
2
+
3
+export function SizeMediumIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+  return (
5
+    <svg viewBox="-4 -4 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
6
+      <path
7
+        d="M8.16191 19H5.68191C5.53525 19 5.46191 18.9267 5.46191 18.78V5H8.76191C8.88191 5 8.97525 5.03333 9.04191 5.1C9.10858 5.15333 9.17525 5.27333 9.24191 5.46C9.72191 6.59333 10.1686 7.7 10.5819 8.78C11.0086 9.84667 11.4352 10.98 11.8619 12.18H12.1619C12.6019 10.9667 13.0352 9.79333 13.4619 8.66C13.8886 7.52667 14.3552 6.30667 14.8619 5H18.3219C18.4686 5 18.5419 5.07333 18.5419 5.22V19H16.0619C15.9152 19 15.8419 18.9267 15.8419 18.78V16.26C15.8419 15.5267 15.8486 14.8133 15.8619 14.12C15.8886 13.4267 15.9286 12.6867 15.9819 11.9C16.0486 11.1 16.1419 10.1933 16.2619 9.18H15.9019C15.4352 10.3533 14.9486 11.5667 14.4419 12.82C13.9486 14.06 13.4819 15.2333 13.0419 16.34H11.1019C11.0619 16.34 11.0152 16.3333 10.9619 16.32C10.9219 16.2933 10.8886 16.2467 10.8619 16.18C10.4619 15.18 10.0086 14.06 9.50191 12.82C9.00858 11.58 8.53525 10.3667 8.08191 9.18H7.70191C7.83525 10.18 7.93525 11.0733 8.00191 11.86C8.06858 12.6467 8.10858 13.3933 8.12191 14.1C8.14858 14.8067 8.16191 15.5267 8.16191 16.26V19Z"
8
+        fill="currentColor"
9
+      />
10
+    </svg>
11
+  )
12
+}

+ 12
- 0
packages/tldraw/src/components/icons/SizeSmallIcon.tsx View File

1
+import * as React from 'react'
2
+
3
+export function SizeSmallIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+  return (
5
+    <svg viewBox="-4 -4 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
6
+      <path
7
+        d="M12.4239 4.62C13.3572 4.62 14.1572 4.73333 14.8239 4.96C15.4906 5.17333 15.9772 5.43333 16.2839 5.74C16.3639 5.82 16.4039 5.94 16.4039 6.1V8.86H14.0639C13.9172 8.86 13.8439 8.78666 13.8439 8.64V7.26C13.4306 7.12666 12.9572 7.06 12.4239 7.06C11.6506 7.06 11.0639 7.18 10.6639 7.42C10.2639 7.66 10.0639 8.04666 10.0639 8.58V9C10.0639 9.38666 10.1639 9.69333 10.3639 9.92C10.5772 10.1333 11.0306 10.3467 11.7239 10.56L13.6439 11.14C14.4706 11.38 15.1172 11.66 15.5839 11.98C16.0506 12.3 16.3772 12.68 16.5639 13.12C16.7639 13.5467 16.8639 14.0733 16.8639 14.7V15.62C16.8639 16.7933 16.4039 17.7133 15.4839 18.38C14.5639 19.0467 13.2839 19.38 11.6439 19.38C10.6706 19.38 9.79723 19.2867 9.0239 19.1C8.2639 18.9133 7.71056 18.6533 7.3639 18.32C7.3239 18.28 7.29056 18.24 7.2639 18.2C7.25056 18.1467 7.2439 18.06 7.2439 17.94V15.74H7.6239C8.2239 16.1533 8.85056 16.4533 9.5039 16.64C10.1572 16.8267 10.9306 16.92 11.8239 16.92C12.6506 16.92 13.2506 16.7867 13.6239 16.52C14.0106 16.2533 14.2039 15.9333 14.2039 15.56V14.88C14.2039 14.6667 14.1639 14.48 14.0839 14.32C14.0172 14.16 13.8706 14.0133 13.6439 13.88C13.4172 13.7467 13.0572 13.6067 12.5639 13.46L10.6639 12.88C9.7839 12.6133 9.11056 12.3 8.6439 11.94C8.17723 11.58 7.85056 11.18 7.6639 10.74C7.49056 10.3 7.4039 9.83333 7.4039 9.34V8.38C7.4039 7.64666 7.61056 7 8.0239 6.44C8.43723 5.88 9.01723 5.44 9.7639 5.12C10.5239 4.78666 11.4106 4.62 12.4239 4.62Z"
8
+        fill="currentColor"
9
+      />
10
+    </svg>
11
+  )
12
+}

packages/tldraw/src/components/icons/trash.tsx → packages/tldraw/src/components/icons/TrashIcon.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
 
2
 
3
-function SvgTrash(props: React.SVGProps<SVGSVGElement>): JSX.Element {
3
+export function TrashIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
   return (
4
   return (
5
     <svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
5
     <svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
6
       <path
6
       <path
21
     </svg>
21
     </svg>
22
   )
22
   )
23
 }
23
 }
24
-
25
-export default SvgTrash

packages/tldraw/src/components/icons/undo.tsx → packages/tldraw/src/components/icons/UndoIcon.tsx View File

1
 import * as React from 'react'
1
 import * as React from 'react'
2
 
2
 
3
-function SvgUndo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
3
+export function UndoIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
   return (
4
   return (
5
-    <svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
5
+    <svg viewBox="0 -1 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
6
       <path
6
       <path
7
         fillRule="evenodd"
7
         fillRule="evenodd"
8
         clipRule="evenodd"
8
         clipRule="evenodd"
16
     </svg>
16
     </svg>
17
   )
17
   )
18
 }
18
 }
19
-
20
-export default SvgUndo

+ 14
- 0
packages/tldraw/src/components/icons/index.ts View File

1
+export * from './BoxIcon'
2
+export * from './CheckIcon'
3
+export * from './CircleIcon'
4
+export * from './DashDashedIcon'
5
+export * from './DashDottedIcon'
6
+export * from './DashDrawIcon'
7
+export * from './DashSolidIcon'
8
+export * from './IsFilledIcon'
9
+export * from './RedoIcon'
10
+export * from './TrashIcon'
11
+export * from './UndoIcon'
12
+export * from './SizeSmallIcon'
13
+export * from './SizeMediumIcon'
14
+export * from './SizeLargeIcon'

+ 0
- 5
packages/tldraw/src/components/icons/index.tsx View File

1
-export { default as Redo } from './redo'
2
-export { default as Trash } from './trash'
3
-export { default as Undo } from './undo'
4
-export { default as Check } from './check'
5
-export { default as CircleIcon } from './circle'

+ 0
- 5
packages/tldraw/src/components/index.ts View File

1
-export * from './shared/tooltip'
2
-export * from './shared/kbd'
3
-export * from './shared'
4
-export * from './icons'
5
-export * from './tldraw'

+ 0
- 1
packages/tldraw/src/components/menu/index.ts View File

1
-export * from './menu'

+ 0
- 9
packages/tldraw/src/components/menu/menu.test.tsx View File

1
-import * as React from 'react'
2
-import { Menu } from './menu'
3
-import { renderWithContext } from '~test'
4
-
5
-describe('menu', () => {
6
-  test('mounts component without crashing', () => {
7
-    renderWithContext(<Menu />)
8
-  })
9
-})

+ 0
- 95
packages/tldraw/src/components/menu/menu.tsx View File

1
-import * as React from 'react'
2
-import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
3
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
4
-import {
5
-  floatingContainer,
6
-  DropdownMenuRoot,
7
-  menuContent,
8
-  iconButton,
9
-  breakpoints,
10
-  DropdownMenuButton,
11
-  DropdownMenuSubMenu,
12
-  DropdownMenuDivider,
13
-  iconWrapper,
14
-  Kbd,
15
-} from '~components/shared'
16
-import { useTLDrawContext } from '~hooks'
17
-import { Preferences } from './preferences'
18
-
19
-export const Menu = React.memo(() => {
20
-  const { tlstate } = useTLDrawContext()
21
-
22
-  const handleNew = React.useCallback(() => {
23
-    if (window.confirm('Are you sure you want to start a new project?')) {
24
-      tlstate.newProject()
25
-    }
26
-  }, [tlstate])
27
-
28
-  const handleSave = React.useCallback(() => {
29
-    tlstate.saveProject()
30
-  }, [tlstate])
31
-
32
-  const handleLoad = React.useCallback(() => {
33
-    tlstate.loadProject()
34
-  }, [tlstate])
35
-
36
-  const handleSignOut = React.useCallback(() => {
37
-    tlstate.signOut()
38
-  }, [tlstate])
39
-
40
-  return (
41
-    <div className={floatingContainer()}>
42
-      <DropdownMenuRoot>
43
-        <DropdownMenu.Trigger dir="ltr" className={iconButton({ bp: breakpoints })}>
44
-          <HamburgerMenuIcon />
45
-        </DropdownMenu.Trigger>
46
-        <DropdownMenu.Content dir="ltr" className={menuContent()} sideOffset={8} align="end">
47
-          <DropdownMenuButton onSelect={handleNew}>
48
-            <span>New Project</span>
49
-            <Kbd variant="menu">#N</Kbd>
50
-          </DropdownMenuButton>
51
-          <DropdownMenuDivider dir="ltr" />
52
-          <DropdownMenuButton disabled onSelect={handleLoad}>
53
-            <span>Open...</span>
54
-            <Kbd variant="menu">#L</Kbd>
55
-          </DropdownMenuButton>
56
-          <RecentFiles />
57
-          <DropdownMenuDivider dir="ltr" />
58
-          <DropdownMenuButton disabled onSelect={handleSave}>
59
-            <span>Save</span>
60
-            <Kbd variant="menu">#S</Kbd>
61
-          </DropdownMenuButton>
62
-          <DropdownMenuButton disabled onSelect={handleSave}>
63
-            <span>Save As...</span>
64
-            <Kbd variant="menu">⇧#S</Kbd>
65
-          </DropdownMenuButton>
66
-          <DropdownMenuDivider dir="ltr" />
67
-          <Preferences />
68
-          <DropdownMenuDivider dir="ltr" />
69
-          <DropdownMenuButton disabled onSelect={handleSignOut}>
70
-            <span>Sign Out</span>
71
-            <div className={iconWrapper({ size: 'small' })}>
72
-              <ExitIcon />
73
-            </div>
74
-          </DropdownMenuButton>
75
-        </DropdownMenu.Content>
76
-      </DropdownMenuRoot>
77
-    </div>
78
-  )
79
-})
80
-
81
-function RecentFiles() {
82
-  return (
83
-    <DropdownMenuSubMenu label="Open Recent..." disabled={true}>
84
-      <DropdownMenuButton>
85
-        <span>Project A</span>
86
-      </DropdownMenuButton>
87
-      <DropdownMenuButton>
88
-        <span>Project B</span>
89
-      </DropdownMenuButton>
90
-      <DropdownMenuButton>
91
-        <span>Project C</span>
92
-      </DropdownMenuButton>
93
-    </DropdownMenuSubMenu>
94
-  )
95
-}

+ 0
- 83
packages/tldraw/src/components/menu/preferences.tsx View File

1
-import * as React from 'react'
2
-import {
3
-  DropdownMenuDivider,
4
-  DropdownMenuSubMenu,
5
-  DropdownMenuCheckboxItem,
6
-  Kbd,
7
-} from '~components/shared'
8
-import { useTLDrawContext } from '~hooks'
9
-import type { Data } from '~types'
10
-
11
-const settingsSelector = (s: Data) => s.settings
12
-
13
-export function Preferences() {
14
-  const { tlstate, useSelector } = useTLDrawContext()
15
-
16
-  const settings = useSelector(settingsSelector)
17
-
18
-  const toggleDebugMode = React.useCallback(() => {
19
-    tlstate.setSetting('isDebugMode', (v) => !v)
20
-  }, [tlstate])
21
-
22
-  const toggleDarkMode = React.useCallback(() => {
23
-    tlstate.setSetting('isDarkMode', (v) => !v)
24
-  }, [tlstate])
25
-
26
-  const toggleFocusMode = React.useCallback(() => {
27
-    tlstate.setSetting('isFocusMode', (v) => !v)
28
-  }, [tlstate])
29
-
30
-  const toggleRotateHandle = React.useCallback(() => {
31
-    tlstate.setSetting('showRotateHandles', (v) => !v)
32
-  }, [tlstate])
33
-
34
-  const toggleBoundShapesHandle = React.useCallback(() => {
35
-    tlstate.setSetting('showBindingHandles', (v) => !v)
36
-  }, [tlstate])
37
-
38
-  const toggleisSnapping = React.useCallback(() => {
39
-    tlstate.setSetting('isSnapping', (v) => !v)
40
-  }, [tlstate])
41
-
42
-  const toggleCloneControls = React.useCallback(() => {
43
-    tlstate.setSetting('showCloneHandles', (v) => !v)
44
-  }, [tlstate])
45
-
46
-  return (
47
-    <DropdownMenuSubMenu label="Preferences">
48
-      <DropdownMenuCheckboxItem checked={settings.isDarkMode} onCheckedChange={toggleDarkMode}>
49
-        <span>Dark Mode</span>
50
-        <Kbd variant="menu">#⇧D</Kbd>
51
-      </DropdownMenuCheckboxItem>
52
-      <DropdownMenuCheckboxItem checked={settings.isFocusMode} onCheckedChange={toggleFocusMode}>
53
-        <span>Focus Mode</span>
54
-        <Kbd variant="menu">⇧.</Kbd>
55
-      </DropdownMenuCheckboxItem>
56
-      <DropdownMenuCheckboxItem checked={settings.isDebugMode} onCheckedChange={toggleDebugMode}>
57
-        <span>Debug Mode</span>
58
-      </DropdownMenuCheckboxItem>
59
-      <DropdownMenuDivider />
60
-      <DropdownMenuCheckboxItem
61
-        checked={settings.showRotateHandles}
62
-        onCheckedChange={toggleRotateHandle}
63
-      >
64
-        <span>Rotate Handles</span>
65
-      </DropdownMenuCheckboxItem>
66
-      <DropdownMenuCheckboxItem
67
-        checked={settings.showBindingHandles}
68
-        onCheckedChange={toggleBoundShapesHandle}
69
-      >
70
-        <span>Binding Handles</span>
71
-      </DropdownMenuCheckboxItem>
72
-      <DropdownMenuCheckboxItem
73
-        checked={settings.showCloneHandles}
74
-        onCheckedChange={toggleCloneControls}
75
-      >
76
-        <span>Clone Handles</span>
77
-      </DropdownMenuCheckboxItem>
78
-      <DropdownMenuCheckboxItem checked={settings.isSnapping} onCheckedChange={toggleisSnapping}>
79
-        <span>Always Show Snaps</span>
80
-      </DropdownMenuCheckboxItem>
81
-    </DropdownMenuSubMenu>
82
-  )
83
-}

+ 0
- 1
packages/tldraw/src/components/page-options-dialog/index.ts View File

1
-export * from './page-options-dialog'

+ 0
- 9
packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx View File

1
-import * as React from 'react'
2
-import { PageOptionsDialog } from './page-options-dialog'
3
-import { mockDocument, renderWithContext } from '~test'
4
-
5
-describe('page options dialog', () => {
6
-  test('mounts component without crashing', () => {
7
-    renderWithContext(<PageOptionsDialog page={mockDocument.pages.page1} />)
8
-  })
9
-})

+ 0
- 106
packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx View File

1
-import * as React from 'react'
2
-import * as Dialog from '@radix-ui/react-alert-dialog'
3
-import { MixerVerticalIcon } from '@radix-ui/react-icons'
4
-import {
5
-  breakpoints,
6
-  iconButton,
7
-  dialogOverlay,
8
-  dialogContent,
9
-  rowButton,
10
-  divider,
11
-} from '~components/shared'
12
-import type { Data, TLDrawPage } from '~types'
13
-import { useTLDrawContext } from '~hooks'
14
-
15
-const canDeleteSelector = (s: Data) => {
16
-  return Object.keys(s.document.pages).length > 1
17
-}
18
-
19
-interface PageOptionsDialogProps {
20
-  page: TLDrawPage
21
-  onOpen?: () => void
22
-  onClose?: () => void
23
-}
24
-
25
-export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
26
-  const { tlstate, useSelector } = useTLDrawContext()
27
-
28
-  const [isOpen, setIsOpen] = React.useState(false)
29
-
30
-  const canDelete = useSelector(canDeleteSelector)
31
-
32
-  const rInput = React.useRef<HTMLInputElement>(null)
33
-
34
-  const handleDuplicate = React.useCallback(() => {
35
-    tlstate.duplicatePage(page.id)
36
-    onClose?.()
37
-  }, [tlstate])
38
-
39
-  const handleDelete = React.useCallback(() => {
40
-    if (window.confirm(`Are you sure you want to delete this page?`)) {
41
-      tlstate.deletePage(page.id)
42
-      onClose?.()
43
-    }
44
-  }, [tlstate])
45
-
46
-  const handleOpenChange = React.useCallback(
47
-    (isOpen: boolean) => {
48
-      setIsOpen(isOpen)
49
-
50
-      if (isOpen) {
51
-        onOpen?.()
52
-        return
53
-      }
54
-    },
55
-    [tlstate, name]
56
-  )
57
-
58
-  function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
59
-    e.stopPropagation()
60
-  }
61
-
62
-  // TODO: Replace with text input
63
-  function handleRename() {
64
-    const nextName = window.prompt('New name:', page.name)
65
-    tlstate.renamePage(page.id, nextName || page.name || 'Page')
66
-  }
67
-
68
-  React.useEffect(() => {
69
-    if (isOpen) {
70
-      requestAnimationFrame(() => {
71
-        rInput.current?.focus()
72
-        rInput.current?.select()
73
-      })
74
-    }
75
-  }, [isOpen])
76
-
77
-  return (
78
-    <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>
79
-      <Dialog.Trigger className={iconButton({ bp: breakpoints, size: 'small' })} data-shy="true">
80
-        <MixerVerticalIcon />
81
-      </Dialog.Trigger>
82
-      <Dialog.Overlay className={dialogOverlay()} />
83
-      <Dialog.Content
84
-        className={dialogContent()}
85
-        onKeyDown={stopPropagation}
86
-        onKeyUp={stopPropagation}
87
-      >
88
-        <Dialog.Action className={rowButton({ bp: breakpoints })} onClick={handleRename}>
89
-          Rename
90
-        </Dialog.Action>
91
-        <Dialog.Action className={rowButton({ bp: breakpoints })} onClick={handleDuplicate}>
92
-          Duplicate
93
-        </Dialog.Action>
94
-        <Dialog.Action
95
-          className={rowButton({ bp: breakpoints, warn: true })}
96
-          disabled={!canDelete}
97
-          onClick={handleDelete}
98
-        >
99
-          Delete
100
-        </Dialog.Action>
101
-        <div className={divider()} />
102
-        <Dialog.Cancel className={rowButton({ bp: breakpoints })}>Cancel</Dialog.Cancel>
103
-      </Dialog.Content>
104
-    </Dialog.Root>
105
-  )
106
-}

+ 0
- 1
packages/tldraw/src/components/page-panel/index.ts View File

1
-export * from './page-panel'

+ 0
- 9
packages/tldraw/src/components/page-panel/page-panel.test.tsx View File

1
-import * as React from 'react'
2
-import { PagePanel } from './page-panel'
3
-import { renderWithContext } from '~test'
4
-
5
-describe('page panel', () => {
6
-  test('mounts component without crashing', () => {
7
-    renderWithContext(<PagePanel />)
8
-  })
9
-})

+ 0
- 18
packages/tldraw/src/components/shared/buttons-row.tsx View File

1
-import css from '~styles'
2
-
3
-/* -------------------------------------------------- */
4
-/*                     Buttons Row                    */
5
-/* -------------------------------------------------- */
6
-
7
-export const buttonsRow = css({
8
-  position: 'relative',
9
-  display: 'flex',
10
-  width: '100%',
11
-  background: 'none',
12
-  border: 'none',
13
-  cursor: 'pointer',
14
-  outline: 'none',
15
-  alignItems: 'center',
16
-  justifyContent: 'flex-start',
17
-  padding: 0,
18
-})

+ 0
- 166
packages/tldraw/src/components/shared/context-menu.tsx View File

1
-import * as React from 'react'
2
-import { CheckIcon, ChevronRightIcon } from '@radix-ui/react-icons'
3
-import {
4
-  Root as CMRoot,
5
-  TriggerItem as CMTriggerItem,
6
-  Separator as CMSeparator,
7
-  Item as CMItem,
8
-  Arrow as CMArrow,
9
-  Content as CMContent,
10
-  ItemIndicator as CMItemIndicator,
11
-  CheckboxItem as CMCheckboxItem,
12
-} from '@radix-ui/react-context-menu'
13
-import { breakpoints } from './breakpoints'
14
-import { rowButton } from './row-button'
15
-import { iconButton } from './icon-button'
16
-import { iconWrapper } from './icon-wrapper'
17
-import { menuContent } from './menu'
18
-import css from '~styles'
19
-
20
-/* -------------------------------------------------- */
21
-/*                    Context Menu                   */
22
-/* -------------------------------------------------- */
23
-
24
-export interface ContextMenuRootProps {
25
-  onOpenChange?: (isOpen: boolean) => void
26
-  children: React.ReactNode
27
-}
28
-
29
-export function ContextMenuRoot({ onOpenChange, children }: ContextMenuRootProps): JSX.Element {
30
-  return (
31
-    <CMRoot dir="ltr" onOpenChange={onOpenChange}>
32
-      {children}
33
-    </CMRoot>
34
-  )
35
-}
36
-
37
-export interface ContextMenuSubMenuProps {
38
-  label: string
39
-  children: React.ReactNode
40
-}
41
-
42
-export function ContextMenuSubMenu({ children, label }: ContextMenuSubMenuProps): JSX.Element {
43
-  return (
44
-    <CMRoot dir="ltr">
45
-      <CMTriggerItem dir="ltr" className={rowButton({ bp: breakpoints })}>
46
-        <span>{label}</span>
47
-        <div className={iconWrapper({ size: 'small' })}>
48
-          <ChevronRightIcon />
49
-        </div>
50
-      </CMTriggerItem>
51
-      <CMContent dir="ltr" className={menuContent()} sideOffset={2} alignOffset={-2}>
52
-        {children}
53
-        <ContextMenuArrow offset={13} />
54
-      </CMContent>
55
-    </CMRoot>
56
-  )
57
-}
58
-
59
-const contextMenuDivider = css({
60
-  backgroundColor: '$hover',
61
-  height: 1,
62
-  margin: '$2 -$2',
63
-})
64
-
65
-export const ContextMenuDivider = React.forwardRef<
66
-  React.ElementRef<typeof CMSeparator>,
67
-  React.ComponentProps<typeof CMSeparator>
68
->((props, forwardedRef) => (
69
-  <CMSeparator
70
-    {...props}
71
-    ref={forwardedRef}
72
-    className={contextMenuDivider({ className: props.className })}
73
-  />
74
-))
75
-
76
-const contextMenuArrow = css({
77
-  fill: '$panel',
78
-})
79
-
80
-export const ContextMenuArrow = React.forwardRef<
81
-  React.ElementRef<typeof CMArrow>,
82
-  React.ComponentProps<typeof CMArrow>
83
->((props, forwardedRef) => (
84
-  <CMArrow
85
-    {...props}
86
-    ref={forwardedRef}
87
-    className={contextMenuArrow({ className: props.className })}
88
-  />
89
-))
90
-
91
-export interface ContextMenuButtonProps {
92
-  onSelect?: () => void
93
-  disabled?: boolean
94
-  children: React.ReactNode
95
-}
96
-
97
-export function ContextMenuButton({
98
-  onSelect,
99
-  children,
100
-  disabled = false,
101
-}: ContextMenuButtonProps): JSX.Element {
102
-  return (
103
-    <CMItem
104
-      dir="ltr"
105
-      className={rowButton({ bp: breakpoints })}
106
-      disabled={disabled}
107
-      onSelect={onSelect}
108
-    >
109
-      {children}
110
-    </CMItem>
111
-  )
112
-}
113
-
114
-interface ContextMenuIconButtonProps {
115
-  onSelect: () => void
116
-  disabled?: boolean
117
-  children: React.ReactNode
118
-}
119
-
120
-export function ContextMenuIconButton({
121
-  onSelect,
122
-  children,
123
-  disabled = false,
124
-}: ContextMenuIconButtonProps): JSX.Element {
125
-  return (
126
-    <CMItem
127
-      dir="ltr"
128
-      className={iconButton({ bp: breakpoints })}
129
-      disabled={disabled}
130
-      onSelect={onSelect}
131
-    >
132
-      {children}
133
-    </CMItem>
134
-  )
135
-}
136
-
137
-interface ContextMenuCheckboxItemProps {
138
-  checked: boolean
139
-  disabled?: boolean
140
-  onCheckedChange: (isChecked: boolean) => void
141
-  children: React.ReactNode
142
-}
143
-
144
-export function ContextMenuCheckboxItem({
145
-  checked,
146
-  disabled = false,
147
-  onCheckedChange,
148
-  children,
149
-}: ContextMenuCheckboxItemProps): JSX.Element {
150
-  return (
151
-    <CMCheckboxItem
152
-      dir="ltr"
153
-      className={rowButton({ bp: breakpoints })}
154
-      onCheckedChange={onCheckedChange}
155
-      checked={checked}
156
-      disabled={disabled}
157
-    >
158
-      {children}
159
-      <CMItemIndicator dir="ltr">
160
-        <div className={iconWrapper({ size: 'small' })}>
161
-          <CheckIcon />
162
-        </div>
163
-      </CMItemIndicator>
164
-    </CMCheckboxItem>
165
-  )
166
-}

+ 0
- 51
packages/tldraw/src/components/shared/dialog.tsx View File

1
-import css from '~styles'
2
-
3
-/* -------------------------------------------------- */
4
-/*                       Dialog                       */
5
-/* -------------------------------------------------- */
6
-
7
-export const dialogContent = css({
8
-  position: 'fixed',
9
-  top: '50%',
10
-  left: '50%',
11
-  transform: 'translate(-50%, -50%)',
12
-  minWidth: 240,
13
-  maxWidth: 'fit-content',
14
-  maxHeight: '85vh',
15
-  marginTop: '-5vh',
16
-  pointerEvents: 'all',
17
-  backgroundColor: '$panel',
18
-  border: '1px solid $panel',
19
-  padding: '$0',
20
-  boxShadow: '$4',
21
-  borderRadius: '4px',
22
-  font: '$ui',
23
-
24
-  '&:focus': {
25
-    outline: 'none',
26
-  },
27
-})
28
-
29
-export const dialogOverlay = css({
30
-  backgroundColor: 'rgba(0, 0, 0, .15)',
31
-  position: 'fixed',
32
-  top: 0,
33
-  right: 0,
34
-  bottom: 0,
35
-  left: 0,
36
-})
37
-
38
-export const dialogInputWrapper = css({
39
-  padding: '$4 $2',
40
-})
41
-
42
-export const dialogTitleRow = css({
43
-  display: 'flex',
44
-  padding: '0 0 0 $4',
45
-  alignItems: 'center',
46
-  justifyContent: 'space-between',
47
-
48
-  h3: {
49
-    fontSize: '$1',
50
-  },
51
-})

+ 0
- 205
packages/tldraw/src/components/shared/dropdown-menu.tsx View File

1
-import * as React from 'react'
2
-import { CheckIcon, ChevronRightIcon } from '@radix-ui/react-icons'
3
-import {
4
-  Root as DMRoot,
5
-  TriggerItem as DMTriggerItem,
6
-  Separator as DMSeparator,
7
-  Item as DMItem,
8
-  Arrow as DMArrow,
9
-  Content as DMContent,
10
-  Trigger as DMTrigger,
11
-  ItemIndicator as DMItemIndicator,
12
-  CheckboxItem as DMCheckboxItem,
13
-} from '@radix-ui/react-dropdown-menu'
14
-
15
-import { Tooltip } from './tooltip'
16
-import { breakpoints } from './breakpoints'
17
-import { rowButton } from './row-button'
18
-import { iconButton } from './icon-button'
19
-import { iconWrapper } from './icon-wrapper'
20
-import { menuContent } from './menu'
21
-
22
-import css from '~styles'
23
-
24
-/* -------------------------------------------------- */
25
-/*                    Dropdown Menu                   */
26
-/* -------------------------------------------------- */
27
-
28
-export interface DropdownMenuRootProps {
29
-  isOpen?: boolean
30
-  onOpenChange?: (isOpen: boolean) => void
31
-  children: React.ReactNode
32
-}
33
-
34
-export function DropdownMenuRoot({
35
-  isOpen,
36
-  onOpenChange,
37
-  children,
38
-}: DropdownMenuRootProps): JSX.Element {
39
-  return (
40
-    <DMRoot dir="ltr" open={isOpen} onOpenChange={onOpenChange}>
41
-      {children}
42
-    </DMRoot>
43
-  )
44
-}
45
-
46
-export interface DropdownMenuSubMenuProps {
47
-  label: string
48
-  disabled?: boolean
49
-  children: React.ReactNode
50
-}
51
-
52
-export function DropdownMenuSubMenu({
53
-  children,
54
-  disabled = false,
55
-  label,
56
-}: DropdownMenuSubMenuProps): JSX.Element {
57
-  return (
58
-    <DMRoot dir="ltr">
59
-      <DMTriggerItem dir="ltr" className={rowButton({ bp: breakpoints })} disabled={disabled}>
60
-        <span>{label}</span>
61
-        <div className={iconWrapper({ size: 'small' })}>
62
-          <ChevronRightIcon />
63
-        </div>
64
-      </DMTriggerItem>
65
-      <DMContent dir="ltr" className={menuContent()} sideOffset={2} alignOffset={-2}>
66
-        {children}
67
-        <DropdownMenuArrow offset={13} />
68
-      </DMContent>
69
-    </DMRoot>
70
-  )
71
-}
72
-
73
-export const dropdownMenuDivider = css({
74
-  backgroundColor: '$hover',
75
-  height: 1,
76
-  marginTop: '$2',
77
-  marginRight: '-$2',
78
-  marginBottom: '$2',
79
-  marginLeft: '-$2',
80
-})
81
-
82
-export const DropdownMenuDivider = React.forwardRef<
83
-  React.ElementRef<typeof DMSeparator>,
84
-  React.ComponentProps<typeof DMSeparator>
85
->((props, forwardedRef) => (
86
-  <DMSeparator
87
-    {...props}
88
-    ref={forwardedRef}
89
-    className={dropdownMenuDivider({ className: props.className })}
90
-  />
91
-))
92
-
93
-export const dropdownMenuArrow = css({
94
-  fill: '$panel',
95
-})
96
-
97
-export const DropdownMenuArrow = React.forwardRef<
98
-  React.ElementRef<typeof DMArrow>,
99
-  React.ComponentProps<typeof DMArrow>
100
->((props, forwardedRef) => (
101
-  <DMArrow
102
-    {...props}
103
-    ref={forwardedRef}
104
-    className={dropdownMenuArrow({ className: props.className })}
105
-  />
106
-))
107
-
108
-export interface DropdownMenuButtonProps {
109
-  onSelect?: () => void
110
-  disabled?: boolean
111
-  children: React.ReactNode
112
-}
113
-
114
-export function DropdownMenuButton({
115
-  onSelect,
116
-  children,
117
-  disabled = false,
118
-}: DropdownMenuButtonProps): JSX.Element {
119
-  return (
120
-    <DMItem
121
-      dir="ltr"
122
-      className={rowButton({ bp: breakpoints })}
123
-      disabled={disabled}
124
-      onSelect={onSelect}
125
-    >
126
-      {children}
127
-    </DMItem>
128
-  )
129
-}
130
-
131
-interface DropdownMenuIconButtonProps {
132
-  onSelect: () => void
133
-  disabled?: boolean
134
-  children: React.ReactNode
135
-}
136
-
137
-export function DropdownMenuIconButton({
138
-  onSelect,
139
-  children,
140
-  disabled = false,
141
-}: DropdownMenuIconButtonProps): JSX.Element {
142
-  return (
143
-    <DMItem
144
-      dir="ltr"
145
-      className={iconButton({ bp: breakpoints })}
146
-      disabled={disabled}
147
-      onSelect={onSelect}
148
-    >
149
-      {children}
150
-    </DMItem>
151
-  )
152
-}
153
-
154
-interface DropdownMenuIconTriggerButtonProps {
155
-  label: string
156
-  kbd?: string
157
-  disabled?: boolean
158
-  children: React.ReactNode
159
-}
160
-
161
-export function DropdownMenuIconTriggerButton({
162
-  label,
163
-  kbd,
164
-  children,
165
-  disabled = false,
166
-}: DropdownMenuIconTriggerButtonProps): JSX.Element {
167
-  return (
168
-    <DMTrigger dir="ltr" className={iconButton({ bp: breakpoints })} disabled={disabled}>
169
-      <Tooltip label={label} kbd={kbd}>
170
-        {children}
171
-      </Tooltip>
172
-    </DMTrigger>
173
-  )
174
-}
175
-
176
-interface MenuCheckboxItemProps {
177
-  checked: boolean
178
-  disabled?: boolean
179
-  onCheckedChange: (isChecked: boolean) => void
180
-  children: React.ReactNode
181
-}
182
-
183
-export function DropdownMenuCheckboxItem({
184
-  checked,
185
-  disabled = false,
186
-  onCheckedChange,
187
-  children,
188
-}: MenuCheckboxItemProps): JSX.Element {
189
-  return (
190
-    <DMCheckboxItem
191
-      dir="ltr"
192
-      className={rowButton({ bp: breakpoints })}
193
-      onCheckedChange={onCheckedChange}
194
-      checked={checked}
195
-      disabled={disabled}
196
-    >
197
-      {children}
198
-      <DMItemIndicator dir="ltr">
199
-        <div className={iconWrapper({ size: 'small' })}>
200
-          <CheckIcon />
201
-        </div>
202
-      </DMItemIndicator>
203
-    </DMCheckboxItem>
204
-  )
205
-}

+ 0
- 44
packages/tldraw/src/components/shared/floating-container.tsx View File

1
-import css from '~styles'
2
-
3
-/* -------------------------------------------------- */
4
-/*                 Floating Container                 */
5
-/* -------------------------------------------------- */
6
-
7
-export const floatingContainer = css({
8
-  backgroundColor: '$panel',
9
-  willChange: 'transform',
10
-  border: '1px solid $panel',
11
-  borderRadius: '4px',
12
-  boxShadow: '$4',
13
-  display: 'flex',
14
-  height: 'fit-content',
15
-  padding: '$0',
16
-  pointerEvents: 'all',
17
-  position: 'relative',
18
-  userSelect: 'none',
19
-  zIndex: 200,
20
-  variants: {
21
-    direction: {
22
-      row: {
23
-        flexDirection: 'row',
24
-      },
25
-      column: {
26
-        flexDirection: 'column',
27
-      },
28
-    },
29
-    elevation: {
30
-      0: {
31
-        boxShadow: 'none',
32
-      },
33
-      2: {
34
-        boxShadow: '$2',
35
-      },
36
-      3: {
37
-        boxShadow: '$3',
38
-      },
39
-      4: {
40
-        boxShadow: '$4',
41
-      },
42
-    },
43
-  },
44
-})

+ 0
- 47
packages/tldraw/src/components/shared/icon-wrapper.tsx View File

1
-import css from '~styles'
2
-
3
-/* -------------------------------------------------- */
4
-/*                    Icon Wrapper                    */
5
-/* -------------------------------------------------- */
6
-
7
-export const iconWrapper = css({
8
-  height: '100%',
9
-  borderRadius: '4px',
10
-  marginRight: '1px',
11
-  display: 'grid',
12
-  alignItems: 'center',
13
-  justifyContent: 'center',
14
-  outline: 'none',
15
-  border: 'none',
16
-  pointerEvents: 'all',
17
-  cursor: 'pointer',
18
-  color: '$text',
19
-
20
-  '& svg': {
21
-    height: 22,
22
-    width: 22,
23
-    strokeWidth: 1,
24
-  },
25
-
26
-  '& > *': {
27
-    gridRow: 1,
28
-    gridColumn: 1,
29
-  },
30
-
31
-  variants: {
32
-    size: {
33
-      small: {
34
-        '& svg': {
35
-          height: '16px',
36
-          width: '16px',
37
-        },
38
-      },
39
-      medium: {
40
-        '& svg': {
41
-          height: '22px',
42
-          width: '22px',
43
-        },
44
-      },
45
-    },
46
-  },
47
-})

+ 0
- 13
packages/tldraw/src/components/shared/index.ts View File

1
-export * from './breakpoints'
2
-export * from './buttons-row'
3
-export * from './context-menu'
4
-export * from './dialog'
5
-export * from './dropdown-menu'
6
-export * from './floating-container'
7
-export * from './icon-button'
8
-export * from './icon-wrapper'
9
-export * from './kbd'
10
-export * from './menu'
11
-export * from './radio-group'
12
-export * from './row-button'
13
-export * from './tooltip'

+ 0
- 66
packages/tldraw/src/components/shared/menu.tsx View File

1
-import { breakpoints } from './breakpoints'
2
-import css from '~styles'
3
-import { rowButton } from './row-button'
4
-
5
-/* -------------------------------------------------- */
6
-/*                        Menu                        */
7
-/* -------------------------------------------------- */
8
-
9
-export const menuContent = css({
10
-  position: 'relative',
11
-  overflow: 'hidden',
12
-  userSelect: 'none',
13
-  zIndex: 180,
14
-  minWidth: 180,
15
-  pointerEvents: 'all',
16
-  backgroundColor: '$panel',
17
-  border: '1px solid $panel',
18
-  padding: '$0',
19
-  boxShadow: '$4',
20
-  borderRadius: '4px',
21
-  font: '$ui',
22
-})
23
-
24
-export const divider = css({
25
-  backgroundColor: '$hover',
26
-  height: 1,
27
-  marginTop: '$2',
28
-  marginRight: '-$2',
29
-  marginBottom: '$2',
30
-  marginLeft: '-$2',
31
-})
32
-
33
-export function MenuButton({
34
-  warn,
35
-  onSelect,
36
-  children,
37
-  disabled = false,
38
-}: {
39
-  warn?: boolean
40
-  onSelect?: () => void
41
-  disabled?: boolean
42
-  children: React.ReactNode
43
-}): JSX.Element {
44
-  return (
45
-    <button
46
-      className={rowButton({ bp: breakpoints, warn })}
47
-      disabled={disabled}
48
-      onSelect={onSelect}
49
-    >
50
-      {children}
51
-    </button>
52
-  )
53
-}
54
-
55
-export const menuTextInput = css({
56
-  backgroundColor: '$panel',
57
-  border: 'none',
58
-  padding: '$4 $3',
59
-  width: '100%',
60
-  outline: 'none',
61
-  background: '$input',
62
-  borderRadius: '4px',
63
-  fontFamily: '$ui',
64
-  fontSize: '$1',
65
-  userSelect: 'all',
66
-})

+ 0
- 0
packages/tldraw/src/components/shared/radio-group.tsx View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save