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 2 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

@@ -0,0 +1,21 @@
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,7 +46,7 @@
46 46
     "@radix-ui/react-id": "^0.1.1",
47 47
     "@radix-ui/react-radio-group": "^0.1.1",
48 48
     "@radix-ui/react-tooltip": "^0.1.1",
49
-    "@stitches/core": "^1.2.5",
49
+    "@stitches/react": "^1.2.5",
50 50
     "@tldraw/core": "^0.1.13",
51 51
     "@tldraw/intersect": "^0.1.3",
52 52
     "@tldraw/vec": "^0.1.3",
@@ -55,4 +55,4 @@
55 55
     "rko": "^0.5.25"
56 56
   },
57 57
   "gitHead": "083b36e167b6911927a6b58cbbb830b11b33f00a"
58
-}
58
+}

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

@@ -1,10 +1,10 @@
1 1
 /* eslint-disable */
2 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 6
 filesToCopy.forEach((file) => {
7
-  fs.copyFile(`../../${file}`, `./dist/${file}`, (err) => {
7
+  fs.copyFile(`../../${file}`, `./${file}`, (err) => {
8 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,6 +1,6 @@
1 1
 import * as React from 'react'
2 2
 import { render } from '@testing-library/react'
3
-import { TLDraw } from './tldraw'
3
+import { TLDraw } from './TLDraw'
4 4
 
5 5
 describe('tldraw', () => {
6 6
   test('mounts component without crashing', () => {

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

@@ -1,19 +1,16 @@
1 1
 import * as React from 'react'
2 2
 import { IdProvider } from '@radix-ui/react-id'
3 3
 import { Renderer } from '@tldraw/core'
4
-import css, { dark } from '~styles'
4
+import styled, { dark } from '~styles'
5 5
 import { Data, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types'
6 6
 import { TLDrawState } from '~state'
7 7
 import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks'
8 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 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 15
 // Selectors
19 16
 const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select'
@@ -68,6 +65,26 @@ export interface TLDrawProps {
68 65
    */
69 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 89
    * (optional) A callback to run when the component mounts.
73 90
    */
@@ -88,6 +105,10 @@ export function TLDraw({
88 105
   autofocus = true,
89 106
   showMenu = true,
90 107
   showPages = true,
108
+  showTools = true,
109
+  showZoom = true,
110
+  showStyles = true,
111
+  showUI = true,
91 112
   onMount,
92 113
   onChange,
93 114
   onUserChange,
@@ -120,27 +141,41 @@ export function TLDraw({
120 141
           autofocus={autofocus}
121 142
           showPages={showPages}
122 143
           showMenu={showMenu}
144
+          showStyles={showStyles}
145
+          showZoom={showZoom}
146
+          showTools={showTools}
147
+          showUI={showUI}
123 148
         />
124 149
       </IdProvider>
125 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 167
 function InnerTldraw({
130 168
   id,
131 169
   currentPageId,
132 170
   autofocus,
133 171
   showPages,
134 172
   showMenu,
173
+  showZoom,
174
+  showStyles,
175
+  showTools,
176
+  showUI,
135 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 179
   const { tlstate, useSelector } = useTLDrawContext()
145 180
 
146 181
   const rWrapper = React.useRef<HTMLDivElement>(null)
@@ -209,11 +244,7 @@ function InnerTldraw({
209 244
   }, [currentPageId, tlstate])
210 245
 
211 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 248
       <OneOff focusableRef={rWrapper} autofocus={autofocus} />
218 249
       <ContextMenu>
219 250
         <Renderer
@@ -284,26 +315,25 @@ function InnerTldraw({
284 315
           onKeyUp={tlstate.onKeyUp}
285 316
         />
286 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,7 +358,7 @@ const OneOff = React.memo(
328 358
   }
329 359
 )
330 360
 
331
-const layout = css({
361
+const StyledLayout = styled('div', {
332 362
   position: 'absolute',
333 363
   height: '100%',
334 364
   width: '100%',
@@ -350,7 +380,7 @@ const layout = css({
350 380
   },
351 381
 })
352 382
 
353
-const ui = css({
383
+const StyledUI = styled('div', {
354 384
   position: 'absolute',
355 385
   top: 0,
356 386
   left: 0,
@@ -367,25 +397,6 @@ const ui = css({
367 397
   },
368 398
 })
369 399
 
370
-const spacer = css({
400
+const StyledSpacer = styled('div', {
371 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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,15 @@
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,5 +1,5 @@
1 1
 import * as React from 'react'
2
-import { ContextMenu } from './context-menu'
2
+import { ContextMenu } from './ContextMenu'
3 3
 import { renderWithContext } from '~test'
4 4
 
5 5
 describe('context menu', () => {

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

@@ -0,0 +1,372 @@
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

@@ -0,0 +1 @@
1
+export * from './ContextMenu'

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

@@ -0,0 +1,12 @@
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

@@ -0,0 +1 @@
1
+export * from './Divider'

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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,33 @@
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

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,26 @@
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

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,32 @@
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,10 +1,6 @@
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 4
   position: 'relative',
9 5
   height: '32px',
10 6
   width: '32px',
@@ -12,15 +8,15 @@ export const iconButton = css({
12 8
   borderRadius: '4px',
13 9
   padding: '0',
14 10
   margin: '0',
15
-  display: 'grid',
16
-  alignItems: 'center',
17
-  justifyContent: 'center',
18 11
   outline: 'none',
19 12
   border: 'none',
20 13
   pointerEvents: 'all',
21 14
   fontSize: '$0',
22 15
   color: '$text',
23 16
   cursor: 'pointer',
17
+  display: 'grid',
18
+  alignItems: 'center',
19
+  justifyContent: 'center',
24 20
 
25 21
   '& > *': {
26 22
     gridRow: 1,

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

@@ -0,0 +1 @@
1
+export * from './IconButton'

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

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

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

@@ -0,0 +1 @@
1
+export * from './Kbd'

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

@@ -0,0 +1,17 @@
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

@@ -0,0 +1 @@
1
+export * from './MenuContent'

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

@@ -0,0 +1,30 @@
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

@@ -0,0 +1 @@
1
+export * from './Panel'

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

@@ -0,0 +1,142 @@
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

@@ -0,0 +1 @@
1
+export * from './RowButton'

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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1 @@
1
+export * from './SmallIcon'

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

@@ -0,0 +1,132 @@
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

@@ -0,0 +1 @@
1
+export * from './ToolButton'

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

@@ -0,0 +1,289 @@
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,8 +1,9 @@
1 1
 import * as React from 'react'
2
-import { floatingContainer, rowButton } from '~components/shared'
3
-import css from '~styles'
2
+import styled from '~styles'
4 3
 import type { Data } from '~types'
5 4
 import { useTLDrawContext } from '~hooks'
5
+import { RowButton } from '~components/RowButton'
6
+import { MenuContent } from '~components/MenuContent'
6 7
 
7 8
 const isEmptyCanvasSelector = (s: Data) =>
8 9
   Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
@@ -16,17 +17,16 @@ export const BackToContent = React.memo(() => {
16 17
   if (!isEmptyCanvas) return null
17 18
 
18 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 27
   pointerEvents: 'all',
29 28
   width: 'fit-content',
29
+  minWidth: 0,
30 30
   gridRow: 1,
31 31
   flexGrow: 2,
32 32
   display: 'block',

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

@@ -0,0 +1,22 @@
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,6 +2,7 @@ import * as React from 'react'
2 2
 import {
3 3
   ArrowTopRightIcon,
4 4
   CircleIcon,
5
+  CursorArrowIcon,
5 6
   Pencil1Icon,
6 7
   Pencil2Icon,
7 8
   SquareIcon,
@@ -9,8 +10,8 @@ import {
9 10
 } from '@radix-ui/react-icons'
10 11
 import { Data, TLDrawShapeType } from '~types'
11 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 16
 const activeToolSelector = (s: Data) => s.appState.activeTool
16 17
 
@@ -19,6 +20,10 @@ export const PrimaryTools = React.memo((): JSX.Element => {
19 20
 
20 21
   const activeTool = useSelector(activeToolSelector)
21 22
 
23
+  const selectSelectTool = React.useCallback(() => {
24
+    tlstate.selectTool('select')
25
+  }, [tlstate])
26
+
22 27
   const selectDrawTool = React.useCallback(() => {
23 28
     tlstate.selectTool(TLDrawShapeType.Draw)
24 29
   }, [tlstate])
@@ -44,55 +49,63 @@ export const PrimaryTools = React.memo((): JSX.Element => {
44 49
   }, [tlstate])
45 50
 
46 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 62
         kbd={'2'}
50 63
         label={TLDrawShapeType.Draw}
51
-        onClick={selectDrawTool}
64
+        onSelect={selectDrawTool}
52 65
         isActive={activeTool === TLDrawShapeType.Draw}
53 66
       >
54 67
         <Pencil1Icon />
55
-      </PrimaryButton>
56
-      <PrimaryButton
68
+      </ToolButtonWithTooltip>
69
+      <ToolButtonWithTooltip
57 70
         kbd={'3'}
58 71
         label={TLDrawShapeType.Rectangle}
59
-        onClick={selectRectangleTool}
72
+        onSelect={selectRectangleTool}
60 73
         isActive={activeTool === TLDrawShapeType.Rectangle}
61 74
       >
62 75
         <SquareIcon />
63
-      </PrimaryButton>
64
-      <PrimaryButton
76
+      </ToolButtonWithTooltip>
77
+      <ToolButtonWithTooltip
65 78
         kbd={'4'}
66 79
         label={TLDrawShapeType.Draw}
67
-        onClick={selectEllipseTool}
80
+        onSelect={selectEllipseTool}
68 81
         isActive={activeTool === TLDrawShapeType.Ellipse}
69 82
       >
70 83
         <CircleIcon />
71
-      </PrimaryButton>
72
-      <PrimaryButton
84
+      </ToolButtonWithTooltip>
85
+      <ToolButtonWithTooltip
73 86
         kbd={'5'}
74 87
         label={TLDrawShapeType.Arrow}
75
-        onClick={selectArrowTool}
88
+        onSelect={selectArrowTool}
76 89
         isActive={activeTool === TLDrawShapeType.Arrow}
77 90
       >
78 91
         <ArrowTopRightIcon />
79
-      </PrimaryButton>
80
-      <PrimaryButton
92
+      </ToolButtonWithTooltip>
93
+      <ToolButtonWithTooltip
81 94
         kbd={'6'}
82 95
         label={TLDrawShapeType.Text}
83
-        onClick={selectTextTool}
96
+        onSelect={selectTextTool}
84 97
         isActive={activeTool === TLDrawShapeType.Text}
85 98
       >
86 99
         <TextIcon />
87
-      </PrimaryButton>
88
-      <PrimaryButton
100
+      </ToolButtonWithTooltip>
101
+      <ToolButtonWithTooltip
89 102
         kbd={'7'}
90 103
         label={TLDrawShapeType.Sticky}
91
-        onClick={selectStickyTool}
104
+        onSelect={selectStickyTool}
92 105
         isActive={activeTool === TLDrawShapeType.Sticky}
93 106
       >
94 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,7 +1,8 @@
1 1
 import * as React from 'react'
2 2
 import { useTLDrawContext } from '~hooks'
3 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 7
 const statusSelector = (s: Data) => s.appState.status
7 8
 const activeToolSelector = (s: Data) => s.appState.activeTool
@@ -12,15 +13,15 @@ export function StatusBar(): JSX.Element | null {
12 13
   const activeTool = useSelector(activeToolSelector)
13 14
 
14 15
   return (
15
-    <div className={statusBarContainer({ size: { '@sm': 'small' } })}>
16
-      <div className={section()}>
16
+    <StyledStatusBar bp={breakpoints}>
17
+      <StyledSection>
17 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 25
   height: 40,
25 26
   userSelect: 'none',
26 27
   borderTop: '1px solid $border',
@@ -36,7 +37,7 @@ const statusBarContainer = css({
36 37
   padding: '0 16px',
37 38
 
38 39
   variants: {
39
-    size: {
40
+    bp: {
40 41
       small: {
41 42
         fontSize: '$1',
42 43
       },
@@ -44,7 +45,7 @@ const statusBarContainer = css({
44 45
   },
45 46
 })
46 47
 
47
-const section = css({
48
+const StyledSection = styled('div', {
48 49
   whiteSpace: 'nowrap',
49 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,5 +1,5 @@
1 1
 import * as React from 'react'
2
-import { ToolsPanel } from './tools-panel'
2
+import { ToolsPanel } from './ToolsPanel'
3 3
 import { renderWithContext } from '~test'
4 4
 
5 5
 describe('tools panel', () => {

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

@@ -0,0 +1,78 @@
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

@@ -0,0 +1 @@
1
+export * from './ToolsPanel'

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

@@ -1,7 +1,7 @@
1 1
 import * as RadixTooltip from '@radix-ui/react-tooltip'
2 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 7
 /*                       Tooltip                      */
@@ -25,22 +25,16 @@ export function Tooltip({
25 25
       <RadixTooltip.Trigger asChild={true}>
26 26
         <span>{children}</span>
27 27
       </RadixTooltip.Trigger>
28
-      <RadixTooltip.Content className={content()} side={side} sideOffset={8}>
28
+      <StyledContent side={side} sideOffset={8}>
29 29
         {label}
30 30
         {kbdProp ? <Kbd variant="tooltip">{kbdProp}</Kbd> : null}
31
-        <RadixTooltip.Arrow className={arrow()} />
32
-      </RadixTooltip.Content>
31
+        <StyledArrow />
32
+      </StyledContent>
33 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 38
   borderRadius: 3,
45 39
   padding: '$3 $3 $3 $3',
46 40
   fontSize: '$1',
@@ -53,7 +47,7 @@ const content = css({
53 47
   userSelect: 'none',
54 48
 })
55 49
 
56
-const arrow = css({
50
+const StyledArrow = styled(RadixTooltip.Arrow, {
57 51
   fill: '$tooltipBg',
58 52
   margin: '0 8px',
59 53
 })

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

@@ -0,0 +1 @@
1
+export * from './Tooltip'

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

@@ -0,0 +1,43 @@
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

@@ -0,0 +1,100 @@
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

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,111 @@
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,19 +1,14 @@
1 1
 import * as React from 'react'
2 2
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
3 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 6
 import { useTLDrawContext } from '~hooks'
16 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 13
 const sortedSelector = (s: Data) =>
19 14
   Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
@@ -22,7 +17,7 @@ const currentPageNameSelector = (s: Data) => s.document.pages[s.appState.current
22 17
 
23 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 21
   const { useSelector } = useTLDrawContext()
27 22
 
28 23
   const rIsOpen = React.useRef(false)
@@ -51,14 +46,12 @@ export function PagePanel(): JSX.Element {
51 46
 
52 47
   return (
53 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 53
         {isOpen && <PageMenuContent onClose={handleClose} />}
61
-      </DropdownMenu.Content>
54
+      </DMContent>
62 55
     </DropdownMenu.Root>
63 56
   )
64 57
 }
@@ -86,34 +79,40 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
86 79
     <>
87 80
       <DropdownMenu.RadioGroup value={currentPageId} onValueChange={handleChangePage}>
88 81
         {sortedPages.map((page) => (
89
-          <div className={buttonWithOptions()} key={page.id}>
82
+          <ButtonWithOptions key={page.id}>
90 83
             <DropdownMenu.RadioItem
91
-              className={rowButton({ bp: breakpoints, variant: 'pageButton' })}
84
+              title={page.name || 'Page'}
92 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 97
             </DropdownMenu.RadioItem>
101 98
             <PageOptionsDialog page={page} onClose={onClose} />
102
-          </div>
99
+          </ButtonWithOptions>
103 100
         ))}
104 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 116
   display: 'grid',
118 117
   gridTemplateColumns: '1fr auto',
119 118
   gridAutoFlow: 'column',
@@ -126,3 +125,7 @@ const buttonWithOptions = css({
126 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

@@ -0,0 +1,139 @@
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

@@ -0,0 +1,70 @@
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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,58 @@
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

@@ -0,0 +1,43 @@
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

@@ -0,0 +1 @@
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,381 +0,0 @@
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 +0,0 @@
1
-export * from './context-menu'

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

@@ -0,0 +1,22 @@
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,6 +1,6 @@
1 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 4
   return (
5 5
     <svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
6 6
       <path
@@ -17,4 +17,4 @@ function SvgCheck(props: React.SVGProps<SVGSVGElement>): JSX.Element {
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

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,19 @@
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

@@ -0,0 +1,19 @@
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

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,18 @@
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,8 +1,8 @@
1 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 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 6
       <path
7 7
         fillRule="evenodd"
8 8
         clipRule="evenodd"
@@ -16,5 +16,3 @@ function SvgRedo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
16 16
     </svg>
17 17
   )
18 18
 }
19
-
20
-export default SvgRedo

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

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,12 @@
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,6 +1,6 @@
1 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 4
   return (
5 5
     <svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
6 6
       <path
@@ -21,5 +21,3 @@ function SvgTrash(props: React.SVGProps<SVGSVGElement>): JSX.Element {
21 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,8 +1,8 @@
1 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 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 6
       <path
7 7
         fillRule="evenodd"
8 8
         clipRule="evenodd"
@@ -16,5 +16,3 @@ function SvgUndo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
16 16
     </svg>
17 17
   )
18 18
 }
19
-
20
-export default SvgUndo

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

@@ -0,0 +1,14 @@
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,5 +0,0 @@
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,5 +0,0 @@
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 +0,0 @@
1
-export * from './menu'

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

@@ -1,9 +0,0 @@
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,95 +0,0 @@
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,83 +0,0 @@
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 +0,0 @@
1
-export * from './page-options-dialog'

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

@@ -1,9 +0,0 @@
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,106 +0,0 @@
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 +0,0 @@
1
-export * from './page-panel'

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

@@ -1,9 +0,0 @@
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,18 +0,0 @@
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,166 +0,0 @@
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,51 +0,0 @@
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,205 +0,0 @@
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,44 +0,0 @@
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,47 +0,0 @@
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,13 +0,0 @@
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,66 +0,0 @@
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