From e2369003c6b40f274642f080c9500e7448f45e86 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 3 Nov 2021 16:46:33 +0000 Subject: [PATCH] [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 --- LICENSE => LICENSE.md | 0 packages/tldraw/LICENSE.md | 21 + packages/tldraw/card-repo.png | Bin 0 -> 64086 bytes packages/tldraw/package.json | 4 +- packages/tldraw/scripts/copy-readme.js | 4 +- .../tldraw.test.tsx => TLDraw.test.tsx} | 2 +- .../tldraw/tldraw.tsx => TLDraw.tsx} | 137 ++++--- .../components/ContextMenu/CMIconButton.tsx | 11 + .../components/ContextMenu/CMRowButton.tsx | 11 + .../ContextMenu/CMTriggerButton.tsx | 15 + .../ContextMenu.test.tsx} | 2 +- .../components/ContextMenu/ContextMenu.tsx | 372 +++++++++++++++++ .../src/components/ContextMenu/index.ts | 1 + .../tldraw/src/components/Divider/Divider.tsx | 12 + .../tldraw/src/components/Divider/index.ts | 1 + .../src/components/DropdownMenu/DMArrow.tsx | 5 + .../DropdownMenu/DMCheckboxItem.tsx | 33 ++ .../src/components/DropdownMenu/DMContent.tsx | 36 ++ .../src/components/DropdownMenu/DMDivider.tsx | 11 + .../components/DropdownMenu/DMIconButton.tsx | 23 ++ .../src/components/DropdownMenu/DMItem.tsx | 11 + .../components/DropdownMenu/DMRadioItem.tsx | 26 ++ .../src/components/DropdownMenu/DMSubMenu.tsx | 28 ++ .../components/DropdownMenu/DMTriggerIcon.tsx | 15 + .../src/components/DropdownMenu/index.tsx | 9 + .../components/FocusButton/FocusButton.tsx | 32 ++ .../src/components/FocusButton/index.ts | 0 .../IconButton.tsx} | 14 +- .../tldraw/src/components/IconButton/index.ts | 1 + .../{shared/kbd.tsx => Kbd/Kbd.tsx} | 12 +- packages/tldraw/src/components/Kbd/index.ts | 1 + .../src/components/MenuContent/MenuContent.ts | 17 + .../src/components/MenuContent/index.ts | 1 + .../tldraw/src/components/Panel/Panel.tsx | 30 ++ packages/tldraw/src/components/Panel/index.ts | 1 + .../src/components/RowButton/RowButton.tsx | 142 +++++++ .../tldraw/src/components/RowButton/index.ts | 1 + .../src/components/SmallIcon/SmallIcon.tsx | 27 ++ .../tldraw/src/components/SmallIcon/index.ts | 1 + .../src/components/ToolButton/ToolButton.tsx | 132 ++++++ .../tldraw/src/components/ToolButton/index.ts | 1 + .../components/ToolsPanel/ActionButton.tsx | 289 +++++++++++++ .../BackToContent.tsx} | 16 +- .../src/components/ToolsPanel/LockButton.tsx | 22 + .../PrimaryTools.tsx} | 57 ++- .../StatusBar.tsx} | 17 +- .../ToolsPanel.test.tsx} | 2 +- .../src/components/ToolsPanel/ToolsPanel.tsx | 78 ++++ .../tldraw/src/components/ToolsPanel/index.ts | 1 + .../tooltip.tsx => Tooltip/Tooltip.tsx} | 20 +- .../tldraw/src/components/Tooltip/index.ts | 1 + .../src/components/TopPanel/ColorMenu.tsx | 43 ++ .../src/components/TopPanel/DashMenu.tsx | 100 +++++ .../src/components/TopPanel/FillCheckbox.tsx | 31 ++ .../tldraw/src/components/TopPanel/Menu.tsx | 111 +++++ .../page-panel.tsx => TopPanel/PageMenu.tsx} | 75 ++-- .../components/TopPanel/PageOptionsDialog.tsx | 139 +++++++ .../components/TopPanel/PreferencesMenu.tsx | 70 ++++ .../src/components/TopPanel/SizeMenu.tsx | 39 ++ .../src/components/TopPanel/TopPanel.tsx | 58 +++ .../src/components/TopPanel/ZoomMenu.tsx | 43 ++ .../tldraw/src/components/TopPanel/index.ts | 1 + .../components/{shared => }/breakpoints.tsx | 0 .../components/context-menu/context-menu.tsx | 381 ------------------ .../src/components/context-menu/index.ts | 1 - .../tldraw/src/components/icons/BoxIcon.tsx | 22 + .../icons/{check.tsx => CheckIcon.tsx} | 4 +- .../icons/{circle.tsx => CircleIcon.tsx} | 0 .../src/components/icons/DashDashedIcon.tsx | 17 + .../src/components/icons/DashDottedIcon.tsx | 19 + .../src/components/icons/DashDrawIcon.tsx | 19 + .../src/components/icons/DashSolidIcon.tsx | 9 + .../src/components/icons/IsFilledIcon.tsx | 18 + .../icons/{redo.tsx => RedoIcon.tsx} | 6 +- .../src/components/icons/SizeLargeIcon.tsx | 12 + .../src/components/icons/SizeMediumIcon.tsx | 12 + .../src/components/icons/SizeSmallIcon.tsx | 12 + .../icons/{trash.tsx => TrashIcon.tsx} | 4 +- .../icons/{undo.tsx => UndoIcon.tsx} | 6 +- packages/tldraw/src/components/icons/index.ts | 14 + .../tldraw/src/components/icons/index.tsx | 5 - packages/tldraw/src/components/index.ts | 5 - packages/tldraw/src/components/menu/index.ts | 1 - .../tldraw/src/components/menu/menu.test.tsx | 9 - packages/tldraw/src/components/menu/menu.tsx | 95 ----- .../src/components/menu/preferences.tsx | 83 ---- .../components/page-options-dialog/index.ts | 1 - .../page-options-dialog.test.tsx | 9 - .../page-options-dialog.tsx | 106 ----- .../tldraw/src/components/page-panel/index.ts | 1 - .../components/page-panel/page-panel.test.tsx | 9 - .../src/components/shared/buttons-row.tsx | 18 - .../src/components/shared/context-menu.tsx | 166 -------- .../tldraw/src/components/shared/dialog.tsx | 51 --- .../src/components/shared/dropdown-menu.tsx | 205 ---------- .../components/shared/floating-container.tsx | 44 -- .../src/components/shared/icon-wrapper.tsx | 47 --- .../tldraw/src/components/shared/index.ts | 13 - .../tldraw/src/components/shared/menu.tsx | 66 --- .../src/components/shared/radio-group.tsx | 18 - .../src/components/shared/row-button.tsx | 101 ----- .../style-panel/align-distribute.tsx | 146 ------- .../src/components/style-panel/index.ts | 1 - .../style-panel/quick-color-select.tsx | 50 --- .../style-panel/quick-dash-select.tsx | 56 --- .../style-panel/quick-fill-select.tsx | 37 -- .../style-panel/quick-size-select.tsx | 51 --- .../style-panel/shapes-functions.tsx | 215 ---------- .../style-panel/style-panel.test.tsx | 9 - .../components/style-panel/style-panel.tsx | 100 ----- .../src/components/style-panel/styled.tsx | 165 -------- .../tldraw/src/components/tldraw/index.ts | 1 - .../tools-panel/back-to-content/index.ts | 1 - .../src/components/tools-panel/index.ts | 1 - .../tools-panel/primary-tools/index.ts | 1 - .../tools-panel/status-bar/index.ts | 1 - .../src/components/tools-panel/styled.tsx | 254 ------------ .../components/tools-panel/tools-panel.tsx | 170 -------- .../components/tools-panel/undo-redo/index.ts | 1 - .../tools-panel/undo-redo/undo-redo.tsx | 30 -- .../src/components/tools-panel/zoom/index.ts | 1 - .../src/components/tools-panel/zoom/zoom.tsx | 39 -- .../tldraw/src/hooks/useKeyboardShortcuts.tsx | 4 +- packages/tldraw/src/index.ts | 2 +- .../tldraw/src/shape-utils/group/group.tsx | 13 +- .../tldraw/src/shape-utils/sticky/sticky.tsx | 93 ++--- packages/tldraw/src/shape-utils/text/text.tsx | 20 +- packages/tldraw/src/state/tlstate.ts | 4 +- .../src/state/tool/BaseTool/BaseTool.ts | 5 +- .../src/state/tool/TextTool/TextTool.ts | 6 + packages/tldraw/src/styles/stitches.config.ts | 20 +- www/package.json | 2 +- www/pages/sponsorware.tsx | 46 +-- www/public/sw.js | 2 - www/public/sw.js.map | 2 +- www/public/workbox-a6b3f14f.js | 2 - www/public/worker-zTyDOkV9qnCPfL6KDOfO4.js | 1 - www/styles/stitches.config.ts | 6 +- yarn.lock | 7 +- 139 files changed, 2524 insertions(+), 3066 deletions(-) rename LICENSE => LICENSE.md (100%) create mode 100644 packages/tldraw/LICENSE.md create mode 100644 packages/tldraw/card-repo.png rename packages/tldraw/src/{components/tldraw/tldraw.test.tsx => TLDraw.test.tsx} (95%) rename packages/tldraw/src/{components/tldraw/tldraw.tsx => TLDraw.tsx} (85%) create mode 100644 packages/tldraw/src/components/ContextMenu/CMIconButton.tsx create mode 100644 packages/tldraw/src/components/ContextMenu/CMRowButton.tsx create mode 100644 packages/tldraw/src/components/ContextMenu/CMTriggerButton.tsx rename packages/tldraw/src/components/{context-menu/context-menu.test.tsx => ContextMenu/ContextMenu.test.tsx} (85%) create mode 100644 packages/tldraw/src/components/ContextMenu/ContextMenu.tsx create mode 100644 packages/tldraw/src/components/ContextMenu/index.ts create mode 100644 packages/tldraw/src/components/Divider/Divider.tsx create mode 100644 packages/tldraw/src/components/Divider/index.ts create mode 100644 packages/tldraw/src/components/DropdownMenu/DMArrow.tsx create mode 100644 packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx create mode 100644 packages/tldraw/src/components/DropdownMenu/DMContent.tsx create mode 100644 packages/tldraw/src/components/DropdownMenu/DMDivider.tsx create mode 100644 packages/tldraw/src/components/DropdownMenu/DMIconButton.tsx create mode 100644 packages/tldraw/src/components/DropdownMenu/DMItem.tsx create mode 100644 packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx create mode 100644 packages/tldraw/src/components/DropdownMenu/DMSubMenu.tsx create mode 100644 packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx create mode 100644 packages/tldraw/src/components/DropdownMenu/index.tsx create mode 100644 packages/tldraw/src/components/FocusButton/FocusButton.tsx create mode 100644 packages/tldraw/src/components/FocusButton/index.ts rename packages/tldraw/src/components/{shared/icon-button.tsx => IconButton/IconButton.tsx} (85%) create mode 100644 packages/tldraw/src/components/IconButton/index.ts rename packages/tldraw/src/components/{shared/kbd.tsx => Kbd/Kbd.tsx} (85%) create mode 100644 packages/tldraw/src/components/Kbd/index.ts create mode 100644 packages/tldraw/src/components/MenuContent/MenuContent.ts create mode 100644 packages/tldraw/src/components/MenuContent/index.ts create mode 100644 packages/tldraw/src/components/Panel/Panel.tsx create mode 100644 packages/tldraw/src/components/Panel/index.ts create mode 100644 packages/tldraw/src/components/RowButton/RowButton.tsx create mode 100644 packages/tldraw/src/components/RowButton/index.ts create mode 100644 packages/tldraw/src/components/SmallIcon/SmallIcon.tsx create mode 100644 packages/tldraw/src/components/SmallIcon/index.ts create mode 100644 packages/tldraw/src/components/ToolButton/ToolButton.tsx create mode 100644 packages/tldraw/src/components/ToolButton/index.ts create mode 100644 packages/tldraw/src/components/ToolsPanel/ActionButton.tsx rename packages/tldraw/src/components/{tools-panel/back-to-content/back-to-content.tsx => ToolsPanel/BackToContent.tsx} (63%) create mode 100644 packages/tldraw/src/components/ToolsPanel/LockButton.tsx rename packages/tldraw/src/components/{tools-panel/primary-tools/primary-tools.tsx => ToolsPanel/PrimaryTools.tsx} (66%) rename packages/tldraw/src/components/{tools-panel/status-bar/status-bar.tsx => ToolsPanel/StatusBar.tsx} (76%) rename packages/tldraw/src/components/{tools-panel/tools-panel.test.tsx => ToolsPanel/ToolsPanel.test.tsx} (82%) create mode 100644 packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx create mode 100644 packages/tldraw/src/components/ToolsPanel/index.ts rename packages/tldraw/src/components/{shared/tooltip.tsx => Tooltip/Tooltip.tsx} (75%) create mode 100644 packages/tldraw/src/components/Tooltip/index.ts create mode 100644 packages/tldraw/src/components/TopPanel/ColorMenu.tsx create mode 100644 packages/tldraw/src/components/TopPanel/DashMenu.tsx create mode 100644 packages/tldraw/src/components/TopPanel/FillCheckbox.tsx create mode 100644 packages/tldraw/src/components/TopPanel/Menu.tsx rename packages/tldraw/src/components/{page-panel/page-panel.tsx => TopPanel/PageMenu.tsx} (62%) create mode 100644 packages/tldraw/src/components/TopPanel/PageOptionsDialog.tsx create mode 100644 packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx create mode 100644 packages/tldraw/src/components/TopPanel/SizeMenu.tsx create mode 100644 packages/tldraw/src/components/TopPanel/TopPanel.tsx create mode 100644 packages/tldraw/src/components/TopPanel/ZoomMenu.tsx create mode 100644 packages/tldraw/src/components/TopPanel/index.ts rename packages/tldraw/src/components/{shared => }/breakpoints.tsx (100%) delete mode 100644 packages/tldraw/src/components/context-menu/context-menu.tsx delete mode 100644 packages/tldraw/src/components/context-menu/index.ts create mode 100644 packages/tldraw/src/components/icons/BoxIcon.tsx rename packages/tldraw/src/components/icons/{check.tsx => CheckIcon.tsx} (98%) rename packages/tldraw/src/components/icons/{circle.tsx => CircleIcon.tsx} (100%) create mode 100644 packages/tldraw/src/components/icons/DashDashedIcon.tsx create mode 100644 packages/tldraw/src/components/icons/DashDottedIcon.tsx create mode 100644 packages/tldraw/src/components/icons/DashDrawIcon.tsx create mode 100644 packages/tldraw/src/components/icons/DashSolidIcon.tsx create mode 100644 packages/tldraw/src/components/icons/IsFilledIcon.tsx rename packages/tldraw/src/components/icons/{redo.tsx => RedoIcon.tsx} (70%) create mode 100644 packages/tldraw/src/components/icons/SizeLargeIcon.tsx create mode 100644 packages/tldraw/src/components/icons/SizeMediumIcon.tsx create mode 100644 packages/tldraw/src/components/icons/SizeSmallIcon.tsx rename packages/tldraw/src/components/icons/{trash.tsx => TrashIcon.tsx} (90%) rename packages/tldraw/src/components/icons/{undo.tsx => UndoIcon.tsx} (70%) create mode 100644 packages/tldraw/src/components/icons/index.ts delete mode 100644 packages/tldraw/src/components/icons/index.tsx delete mode 100644 packages/tldraw/src/components/index.ts delete mode 100644 packages/tldraw/src/components/menu/index.ts delete mode 100644 packages/tldraw/src/components/menu/menu.test.tsx delete mode 100644 packages/tldraw/src/components/menu/menu.tsx delete mode 100644 packages/tldraw/src/components/menu/preferences.tsx delete mode 100644 packages/tldraw/src/components/page-options-dialog/index.ts delete mode 100644 packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx delete mode 100644 packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx delete mode 100644 packages/tldraw/src/components/page-panel/index.ts delete mode 100644 packages/tldraw/src/components/page-panel/page-panel.test.tsx delete mode 100644 packages/tldraw/src/components/shared/buttons-row.tsx delete mode 100644 packages/tldraw/src/components/shared/context-menu.tsx delete mode 100644 packages/tldraw/src/components/shared/dialog.tsx delete mode 100644 packages/tldraw/src/components/shared/dropdown-menu.tsx delete mode 100644 packages/tldraw/src/components/shared/floating-container.tsx delete mode 100644 packages/tldraw/src/components/shared/icon-wrapper.tsx delete mode 100644 packages/tldraw/src/components/shared/index.ts delete mode 100644 packages/tldraw/src/components/shared/menu.tsx delete mode 100644 packages/tldraw/src/components/shared/radio-group.tsx delete mode 100644 packages/tldraw/src/components/shared/row-button.tsx delete mode 100644 packages/tldraw/src/components/style-panel/align-distribute.tsx delete mode 100644 packages/tldraw/src/components/style-panel/index.ts delete mode 100644 packages/tldraw/src/components/style-panel/quick-color-select.tsx delete mode 100644 packages/tldraw/src/components/style-panel/quick-dash-select.tsx delete mode 100644 packages/tldraw/src/components/style-panel/quick-fill-select.tsx delete mode 100644 packages/tldraw/src/components/style-panel/quick-size-select.tsx delete mode 100644 packages/tldraw/src/components/style-panel/shapes-functions.tsx delete mode 100644 packages/tldraw/src/components/style-panel/style-panel.test.tsx delete mode 100644 packages/tldraw/src/components/style-panel/style-panel.tsx delete mode 100644 packages/tldraw/src/components/style-panel/styled.tsx delete mode 100644 packages/tldraw/src/components/tldraw/index.ts delete mode 100644 packages/tldraw/src/components/tools-panel/back-to-content/index.ts delete mode 100644 packages/tldraw/src/components/tools-panel/index.ts delete mode 100644 packages/tldraw/src/components/tools-panel/primary-tools/index.ts delete mode 100644 packages/tldraw/src/components/tools-panel/status-bar/index.ts delete mode 100644 packages/tldraw/src/components/tools-panel/styled.tsx delete mode 100644 packages/tldraw/src/components/tools-panel/tools-panel.tsx delete mode 100644 packages/tldraw/src/components/tools-panel/undo-redo/index.ts delete mode 100644 packages/tldraw/src/components/tools-panel/undo-redo/undo-redo.tsx delete mode 100644 packages/tldraw/src/components/tools-panel/zoom/index.ts delete mode 100644 packages/tldraw/src/components/tools-panel/zoom/zoom.tsx delete mode 100644 www/public/sw.js delete mode 100644 www/public/workbox-a6b3f14f.js delete mode 100644 www/public/worker-zTyDOkV9qnCPfL6KDOfO4.js diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/packages/tldraw/LICENSE.md b/packages/tldraw/LICENSE.md new file mode 100644 index 000000000..bdcc8b850 --- /dev/null +++ b/packages/tldraw/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Stephen Ruiz Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/tldraw/card-repo.png b/packages/tldraw/card-repo.png new file mode 100644 index 0000000000000000000000000000000000000000..2b3997cbd271bba0ee90cf72adb1c6417fd9cc7e GIT binary patch literal 64086 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

tzi4p4|vj7-Zt#|pE6*I^I<4| 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 { - fs.copyFile(`../../${file}`, `./dist/${file}`, (err) => { + fs.copyFile(`../../${file}`, `./${file}`, (err) => { if (err) throw err }) }) diff --git a/packages/tldraw/src/components/tldraw/tldraw.test.tsx b/packages/tldraw/src/TLDraw.test.tsx similarity index 95% rename from packages/tldraw/src/components/tldraw/tldraw.test.tsx rename to packages/tldraw/src/TLDraw.test.tsx index c04d83c0a..ab258ef27 100644 --- a/packages/tldraw/src/components/tldraw/tldraw.test.tsx +++ b/packages/tldraw/src/TLDraw.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { render } from '@testing-library/react' -import { TLDraw } from './tldraw' +import { TLDraw } from './TLDraw' describe('tldraw', () => { test('mounts component without crashing', () => { diff --git a/packages/tldraw/src/components/tldraw/tldraw.tsx b/packages/tldraw/src/TLDraw.tsx similarity index 85% rename from packages/tldraw/src/components/tldraw/tldraw.tsx rename to packages/tldraw/src/TLDraw.tsx index 1b024fa87..1e0e782e6 100644 --- a/packages/tldraw/src/components/tldraw/tldraw.tsx +++ b/packages/tldraw/src/TLDraw.tsx @@ -1,19 +1,16 @@ import * as React from 'react' import { IdProvider } from '@radix-ui/react-id' import { Renderer } from '@tldraw/core' -import css, { dark } from '~styles' +import styled, { dark } from '~styles' import { Data, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types' import { TLDrawState } from '~state' import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks' import { shapeUtils } from '~shape-utils' -import { StylePanel } from '~components/style-panel' -import { ToolsPanel } from '~components/tools-panel' -import { PagePanel } from '~components/page-panel' -import { Menu } from '~components/menu' -import { breakpoints, iconButton } from '~components' -import { DotFilledIcon } from '@radix-ui/react-icons' +import { ToolsPanel } from '~components/ToolsPanel' +import { TopPanel } from '~components/TopPanel' import { TLDR } from '~state/tldr' -import { ContextMenu } from '~components/context-menu' +import { ContextMenu } from '~components/ContextMenu' +import { FocusButton } from '~components/FocusButton/FocusButton' // Selectors const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select' @@ -68,6 +65,26 @@ export interface TLDrawProps { */ showPages?: boolean + /** + * (optional) Whether to show the styles UI. + */ + showStyles?: boolean + + /** + * (optional) Whether to show the zoom UI. + */ + showZoom?: boolean + + /** + * (optional) Whether to show the tools UI. + */ + showTools?: boolean + + /** + * (optional) Whether to show the UI. + */ + showUI?: boolean + /** * (optional) A callback to run when the component mounts. */ @@ -88,6 +105,10 @@ export function TLDraw({ autofocus = true, showMenu = true, showPages = true, + showTools = true, + showZoom = true, + showStyles = true, + showUI = true, onMount, onChange, onUserChange, @@ -120,27 +141,41 @@ export function TLDraw({ autofocus={autofocus} showPages={showPages} showMenu={showMenu} + showStyles={showStyles} + showZoom={showZoom} + showTools={showTools} + showUI={showUI} /> ) } +interface InnerTLDrawProps { + id?: string + currentPageId?: string + autofocus: boolean + showPages: boolean + showMenu: boolean + showZoom: boolean + showStyles: boolean + showUI: boolean + showTools: boolean + document?: TLDrawDocument +} + function InnerTldraw({ id, currentPageId, autofocus, showPages, showMenu, + showZoom, + showStyles, + showTools, + showUI, document, -}: { - id?: string - currentPageId?: string - autofocus: boolean - showPages: boolean - showMenu: boolean - document?: TLDrawDocument -}) { +}: InnerTLDrawProps) { const { tlstate, useSelector } = useTLDrawContext() const rWrapper = React.useRef(null) @@ -209,11 +244,7 @@ function InnerTldraw({ }, [currentPageId, tlstate]) return ( -

+ -
- {settings.isFocusMode ? ( -
- -
- ) : ( - <> -
- {showMenu && } - {showPages && } -
-
- - - - )} -
-
+ {showUI && ( + + {settings.isFocusMode ? ( + + ) : ( + <> + + + {showTools && } + + )} + + )} +
) } @@ -328,7 +358,7 @@ const OneOff = React.memo( } ) -const layout = css({ +const StyledLayout = styled('div', { position: 'absolute', height: '100%', width: '100%', @@ -350,7 +380,7 @@ const layout = css({ }, }) -const ui = css({ +const StyledUI = styled('div', { position: 'absolute', top: 0, left: 0, @@ -367,25 +397,6 @@ const ui = css({ }, }) -const spacer = css({ +const StyledSpacer = styled('div', { flexGrow: 2, }) - -const menuButtons = css({ - display: 'flex', - gap: 8, -}) - -const unfocusButton = css({ - opacity: 1, - zIndex: 100, - backgroundColor: 'transparent', - - '& svg': { - color: '$muted', - }, - - '&:hover svg': { - color: '$text', - }, -}) diff --git a/packages/tldraw/src/components/ContextMenu/CMIconButton.tsx b/packages/tldraw/src/components/ContextMenu/CMIconButton.tsx new file mode 100644 index 000000000..9f6a45afc --- /dev/null +++ b/packages/tldraw/src/components/ContextMenu/CMIconButton.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' +import { ContextMenuItem } from '@radix-ui/react-context-menu' +import { ToolButton, ToolButtonProps } from '~components/ToolButton' + +export function CMIconButton({ onSelect, ...rest }: ToolButtonProps): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/ContextMenu/CMRowButton.tsx b/packages/tldraw/src/components/ContextMenu/CMRowButton.tsx new file mode 100644 index 000000000..ab0405ed0 --- /dev/null +++ b/packages/tldraw/src/components/ContextMenu/CMRowButton.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' +import { ContextMenuItem } from '@radix-ui/react-context-menu' +import { RowButton, RowButtonProps } from '~components/RowButton' + +export const CMRowButton = ({ onSelect, ...rest }: RowButtonProps) => { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/ContextMenu/CMTriggerButton.tsx b/packages/tldraw/src/components/ContextMenu/CMTriggerButton.tsx new file mode 100644 index 000000000..920a100f6 --- /dev/null +++ b/packages/tldraw/src/components/ContextMenu/CMTriggerButton.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import { ContextMenuTriggerItem } from '@radix-ui/react-context-menu' +import { RowButton, RowButtonProps } from '~components/RowButton' + +interface CMTriggerButtonProps extends RowButtonProps { + isSubmenu?: boolean +} + +export const CMTriggerButton = ({ isSubmenu, ...rest }: CMTriggerButtonProps) => { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/context-menu/context-menu.test.tsx b/packages/tldraw/src/components/ContextMenu/ContextMenu.test.tsx similarity index 85% rename from packages/tldraw/src/components/context-menu/context-menu.test.tsx rename to packages/tldraw/src/components/ContextMenu/ContextMenu.test.tsx index e68ce3b5a..a956984d4 100644 --- a/packages/tldraw/src/components/context-menu/context-menu.test.tsx +++ b/packages/tldraw/src/components/ContextMenu/ContextMenu.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { ContextMenu } from './context-menu' +import { ContextMenu } from './ContextMenu' import { renderWithContext } from '~test' describe('context menu', () => { diff --git a/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx b/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx new file mode 100644 index 000000000..ae2486798 --- /dev/null +++ b/packages/tldraw/src/components/ContextMenu/ContextMenu.tsx @@ -0,0 +1,372 @@ +import * as React from 'react' +import styled from '~styles' +import * as RadixContextMenu from '@radix-ui/react-context-menu' +import { useTLDrawContext } from '~hooks' +import { Data, AlignType, DistributeType, StretchType } from '~types' +import { + AlignBottomIcon, + AlignCenterHorizontallyIcon, + AlignCenterVerticallyIcon, + AlignLeftIcon, + AlignRightIcon, + AlignTopIcon, + SpaceEvenlyHorizontallyIcon, + SpaceEvenlyVerticallyIcon, + StretchHorizontallyIcon, + StretchVerticallyIcon, +} from '@radix-ui/react-icons' +import { CMRowButton } from './CMRowButton' +import { CMIconButton } from './CMIconButton' +import { CMTriggerButton } from './CMTriggerButton' +import { Divider } from '~components/Divider' +import { MenuContent } from '~components/MenuContent' + +const has1SelectedIdsSelector = (s: Data) => { + return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0 +} +const has2SelectedIdsSelector = (s: Data) => { + return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1 +} +const has3SelectedIdsSelector = (s: Data) => { + return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2 +} + +const isDebugModeSelector = (s: Data) => { + return s.settings.isDebugMode +} + +const hasGroupSelectedSelector = (s: Data) => { + return s.document.pageStates[s.appState.currentPageId].selectedIds.some( + (id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined + ) +} + +interface ContextMenuProps { + children: React.ReactNode +} + +export const ContextMenu = ({ children }: ContextMenuProps): JSX.Element => { + const { tlstate, useSelector } = useTLDrawContext() + const hasSelection = useSelector(has1SelectedIdsSelector) + const hasTwoOrMore = useSelector(has2SelectedIdsSelector) + const hasThreeOrMore = useSelector(has3SelectedIdsSelector) + const isDebugMode = useSelector(isDebugModeSelector) + const hasGroupSelected = useSelector(hasGroupSelectedSelector) + + const rContent = React.useRef(null) + + const handleFlipHorizontal = React.useCallback(() => { + tlstate.flipHorizontal() + }, [tlstate]) + + const handleFlipVertical = React.useCallback(() => { + tlstate.flipVertical() + }, [tlstate]) + + const handleDuplicate = React.useCallback(() => { + tlstate.duplicate() + }, [tlstate]) + + const handleGroup = React.useCallback(() => { + tlstate.group() + }, [tlstate]) + + const handleMoveToBack = React.useCallback(() => { + tlstate.moveToBack() + }, [tlstate]) + + const handleMoveBackward = React.useCallback(() => { + tlstate.moveBackward() + }, [tlstate]) + + const handleMoveForward = React.useCallback(() => { + tlstate.moveForward() + }, [tlstate]) + + const handleMoveToFront = React.useCallback(() => { + tlstate.moveToFront() + }, [tlstate]) + + const handleDelete = React.useCallback(() => { + tlstate.delete() + }, [tlstate]) + + const handleCopyJson = React.useCallback(() => { + tlstate.copyJson() + }, [tlstate]) + + const handleCopy = React.useCallback(() => { + tlstate.copy() + }, [tlstate]) + + const handlePaste = React.useCallback(() => { + tlstate.paste() + }, [tlstate]) + + const handleCopySvg = React.useCallback(() => { + tlstate.copySvg() + }, [tlstate]) + + const handleUndo = React.useCallback(() => { + tlstate.undo() + }, [tlstate]) + + const handleRedo = React.useCallback(() => { + tlstate.redo() + }, [tlstate]) + + return ( + + {children} + + + {hasSelection ? ( + <> + + Flip Horizontal + + + Flip Vertical + + + Duplicate + + + {hasTwoOrMore && ( + + Group + + )} + + {hasGroupSelected && ( + + Ungroup + + )} + + + To Front + + + Forward + + + Backward + + + To Back + + + + {hasTwoOrMore && ( + + )} + + + Copy + + + Copy as SVG + + {isDebugMode && Copy as JSON} + + Paste + + + + Delete + + + ) : ( + <> + + Paste + + + Undo + + + Redo + + + )} + + + + ) +} + +function AlignDistributeSubMenu({ + hasThreeOrMore, +}: { + hasTwoOrMore: boolean + hasThreeOrMore: boolean +}) { + const { tlstate } = useTLDrawContext() + + const alignTop = React.useCallback(() => { + tlstate.align(AlignType.Top) + }, [tlstate]) + + const alignCenterVertical = React.useCallback(() => { + tlstate.align(AlignType.CenterVertical) + }, [tlstate]) + + const alignBottom = React.useCallback(() => { + tlstate.align(AlignType.Bottom) + }, [tlstate]) + + const stretchVertically = React.useCallback(() => { + tlstate.stretch(StretchType.Vertical) + }, [tlstate]) + + const distributeVertically = React.useCallback(() => { + tlstate.distribute(DistributeType.Vertical) + }, [tlstate]) + + const alignLeft = React.useCallback(() => { + tlstate.align(AlignType.Left) + }, [tlstate]) + + const alignCenterHorizontal = React.useCallback(() => { + tlstate.align(AlignType.CenterHorizontal) + }, [tlstate]) + + const alignRight = React.useCallback(() => { + tlstate.align(AlignType.Right) + }, [tlstate]) + + const stretchHorizontally = React.useCallback(() => { + tlstate.stretch(StretchType.Horizontal) + }, [tlstate]) + + const distributeHorizontally = React.useCallback(() => { + tlstate.distribute(DistributeType.Horizontal) + }, [tlstate]) + + return ( + + Align / Distribute + + + + + + + + + + + + + + + {hasThreeOrMore && ( + + + + )} + + + + + + + + + + + + + {hasThreeOrMore && ( + + + + )} + + + + + ) +} + +const StyledGridContent = styled(MenuContent, { + display: 'grid', + variants: { + selectedStyle: { + threeOrMore: { + gridTemplateColumns: 'repeat(5, auto)', + }, + twoOrMore: { + gridTemplateColumns: 'repeat(4, auto)', + }, + }, + }, +}) + +/* ------------------ Move to Page ------------------ */ + +const currentPageIdSelector = (s: Data) => s.appState.currentPageId +const documentPagesSelector = (s: Data) => s.document.pages + +function MoveToPageMenu(): JSX.Element | null { + const { tlstate, useSelector } = useTLDrawContext() + const currentPageId = useSelector(currentPageIdSelector) + const documentPages = useSelector(documentPagesSelector) + + const sorted = Object.values(documentPages) + .sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0)) + .filter((a) => a.id !== currentPageId) + + if (sorted.length === 0) return null + + return ( + + Move To Page + + + {sorted.map(({ id, name }, i) => ( + tlstate.moveToPage(id)} + > + {name || `Page ${i}`} + + ))} + + + + + ) +} + +/* --------------------- Submenu -------------------- */ + +export interface ContextMenuSubMenuProps { + label: string + children: React.ReactNode +} + +export function ContextMenuSubMenu({ children, label }: ContextMenuSubMenuProps): JSX.Element { + return ( + + {label} + + + {children} + + + + + ) +} + +/* ---------------------- Arrow --------------------- */ + +const CMArrow = styled(RadixContextMenu.ContextMenuArrow, { + fill: '$panel', +}) diff --git a/packages/tldraw/src/components/ContextMenu/index.ts b/packages/tldraw/src/components/ContextMenu/index.ts new file mode 100644 index 000000000..47f487393 --- /dev/null +++ b/packages/tldraw/src/components/ContextMenu/index.ts @@ -0,0 +1 @@ +export * from './ContextMenu' diff --git a/packages/tldraw/src/components/Divider/Divider.tsx b/packages/tldraw/src/components/Divider/Divider.tsx new file mode 100644 index 000000000..406840659 --- /dev/null +++ b/packages/tldraw/src/components/Divider/Divider.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' +import styled from '~styles' + +export const Divider = styled('hr', { + height: 1, + marginTop: '$1', + marginRight: '-$2', + marginBottom: '$1', + marginLeft: '-$2', + border: 'none', + borderBottom: '1px solid $hover', +}) diff --git a/packages/tldraw/src/components/Divider/index.ts b/packages/tldraw/src/components/Divider/index.ts new file mode 100644 index 000000000..1b8726417 --- /dev/null +++ b/packages/tldraw/src/components/Divider/index.ts @@ -0,0 +1 @@ +export * from './Divider' diff --git a/packages/tldraw/src/components/DropdownMenu/DMArrow.tsx b/packages/tldraw/src/components/DropdownMenu/DMArrow.tsx new file mode 100644 index 000000000..db5452d7a --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/DMArrow.tsx @@ -0,0 +1,5 @@ +import { Arrow } from '@radix-ui/react-dropdown-menu' +import { breakpoints } from '~components/breakpoints' +import styled from '~styles/stitches.config' + +export const DMArrow = styled(Arrow, { fill: '$panel', bp: breakpoints }) diff --git a/packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx b/packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx new file mode 100644 index 000000000..f8dc4b50c --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/DMCheckboxItem.tsx @@ -0,0 +1,33 @@ +import * as React from 'react' +import { CheckboxItem } from '@radix-ui/react-dropdown-menu' +import { RowButton } from '~components/RowButton' + +interface DMCheckboxItemProps { + checked: boolean + disabled?: boolean + onCheckedChange: (isChecked: boolean) => void + children: React.ReactNode + kbd?: string +} + +export function DMCheckboxItem({ + checked, + disabled = false, + onCheckedChange, + kbd, + children, +}: DMCheckboxItemProps): JSX.Element { + return ( + + + {children} + + + ) +} diff --git a/packages/tldraw/src/components/DropdownMenu/DMContent.tsx b/packages/tldraw/src/components/DropdownMenu/DMContent.tsx new file mode 100644 index 000000000..5b5fb19c6 --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/DMContent.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { Content } from '@radix-ui/react-dropdown-menu' +import styled from '~styles/stitches.config' +import { MenuContent } from '~components/MenuContent' + +export interface DMContentProps { + variant?: 'grid' | 'menu' + align?: 'start' | 'center' | 'end' + children: React.ReactNode +} + +export function DMContent({ children, align, variant }: DMContentProps): JSX.Element { + return ( + + {children} + + ) +} + +export const StyledContent = styled(MenuContent, { + width: 'fit-content', + height: 'fit-content', + minWidth: 0, + variants: { + variant: { + grid: { + display: 'grid', + gridTemplateColumns: 'repeat(4, auto)', + gap: 0, + }, + menu: { + minWidth: 128, + }, + }, + }, +}) diff --git a/packages/tldraw/src/components/DropdownMenu/DMDivider.tsx b/packages/tldraw/src/components/DropdownMenu/DMDivider.tsx new file mode 100644 index 000000000..6e53c348c --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/DMDivider.tsx @@ -0,0 +1,11 @@ +import { Separator } from '@radix-ui/react-dropdown-menu' +import styled from '~styles/stitches.config' + +export const DMDivider = styled(Separator, { + backgroundColor: '$hover', + height: 1, + marginTop: '$2', + marginRight: '-$2', + marginBottom: '$2', + marginLeft: '-$2', +}) diff --git a/packages/tldraw/src/components/DropdownMenu/DMIconButton.tsx b/packages/tldraw/src/components/DropdownMenu/DMIconButton.tsx new file mode 100644 index 000000000..05c347ca6 --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/DMIconButton.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import { Item } from '@radix-ui/react-dropdown-menu' +import { IconButton } from '~components/IconButton/IconButton' + +interface DMIconButtonProps { + onSelect: () => void + disabled?: boolean + children: React.ReactNode +} + +export function DMIconButton({ + onSelect, + children, + disabled = false, +}: DMIconButtonProps): JSX.Element { + return ( + + + {children} + + + ) +} diff --git a/packages/tldraw/src/components/DropdownMenu/DMItem.tsx b/packages/tldraw/src/components/DropdownMenu/DMItem.tsx new file mode 100644 index 000000000..8388a4c0e --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/DMItem.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' +import { Item } from '@radix-ui/react-dropdown-menu' +import { RowButton, RowButtonProps } from '~components/RowButton' + +export function DMItem({ onSelect, ...rest }: RowButtonProps): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx b/packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx new file mode 100644 index 000000000..aa1915638 --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx @@ -0,0 +1,26 @@ +import { RadioItem } from '@radix-ui/react-dropdown-menu' +import styled from '~styles/stitches.config' + +export const DMRadioItem = styled(RadioItem, { + height: '32px', + width: '32px', + backgroundColor: '$panel', + borderRadius: '4px', + padding: '0', + margin: '0', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + outline: 'none', + border: 'none', + pointerEvents: 'all', + cursor: 'pointer', + + '&:focus': { + backgroundColor: '$hover', + }, + + '&:hover:not(:disabled)': { + backgroundColor: '$hover', + }, +}) diff --git a/packages/tldraw/src/components/DropdownMenu/DMSubMenu.tsx b/packages/tldraw/src/components/DropdownMenu/DMSubMenu.tsx new file mode 100644 index 000000000..af3ff7e54 --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/DMSubMenu.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import { Root, TriggerItem, Content, Arrow } from '@radix-ui/react-dropdown-menu' +import { RowButton } from '~components/RowButton' +import { MenuContent } from '~components/MenuContent' + +export interface DMSubMenuProps { + label: string + disabled?: boolean + children: React.ReactNode +} + +export function DMSubMenu({ children, disabled = false, label }: DMSubMenuProps): JSX.Element { + return ( + + + + {label} + + + + + {children} + + + + + ) +} diff --git a/packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx b/packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx new file mode 100644 index 000000000..ba0018784 --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/DMTriggerIcon.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import { Trigger } from '@radix-ui/react-dropdown-menu' +import { ToolButton } from '~components/ToolButton' + +interface DMTriggerIconProps { + children: React.ReactNode +} + +export function DMTriggerIcon({ children }: DMTriggerIconProps) { + return ( + + {children} + + ) +} diff --git a/packages/tldraw/src/components/DropdownMenu/index.tsx b/packages/tldraw/src/components/DropdownMenu/index.tsx new file mode 100644 index 000000000..232f0c0d9 --- /dev/null +++ b/packages/tldraw/src/components/DropdownMenu/index.tsx @@ -0,0 +1,9 @@ +export * from './DMArrow' +export * from './DMItem' +export * from './DMCheckboxItem' +export * from './DMContent' +export * from './DMDivider' +export * from './DMIconButton' +export * from './DMRadioItem' +export * from './DMSubMenu' +export * from './DMTriggerIcon' diff --git a/packages/tldraw/src/components/FocusButton/FocusButton.tsx b/packages/tldraw/src/components/FocusButton/FocusButton.tsx new file mode 100644 index 000000000..b214fb7f8 --- /dev/null +++ b/packages/tldraw/src/components/FocusButton/FocusButton.tsx @@ -0,0 +1,32 @@ +import { DotFilledIcon } from '@radix-ui/react-icons' +import * as React from 'react' +import { IconButton } from '~components/IconButton/IconButton' +import styled from '~styles' + +interface FocusButtonProps { + onSelect: () => void +} + +export function FocusButton({ onSelect }: FocusButtonProps) { + return ( + + + + + + ) +} + +const StyledButtonContainer = styled('div', { + opacity: 1, + zIndex: 100, + backgroundColor: 'transparent', + + '& svg': { + color: '$muted', + }, + + '&:hover svg': { + color: '$text', + }, +}) diff --git a/packages/tldraw/src/components/FocusButton/index.ts b/packages/tldraw/src/components/FocusButton/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/tldraw/src/components/shared/icon-button.tsx b/packages/tldraw/src/components/IconButton/IconButton.tsx similarity index 85% rename from packages/tldraw/src/components/shared/icon-button.tsx rename to packages/tldraw/src/components/IconButton/IconButton.tsx index 7c6fbfcd9..63471636b 100644 --- a/packages/tldraw/src/components/shared/icon-button.tsx +++ b/packages/tldraw/src/components/IconButton/IconButton.tsx @@ -1,10 +1,6 @@ -import css from '~styles' +import styled from '~styles' -/* -------------------------------------------------- */ -/* Icon Button */ -/* -------------------------------------------------- */ - -export const iconButton = css({ +export const IconButton = styled('button', { position: 'relative', height: '32px', width: '32px', @@ -12,15 +8,15 @@ export const iconButton = css({ borderRadius: '4px', padding: '0', margin: '0', - display: 'grid', - alignItems: 'center', - justifyContent: 'center', outline: 'none', border: 'none', pointerEvents: 'all', fontSize: '$0', color: '$text', cursor: 'pointer', + display: 'grid', + alignItems: 'center', + justifyContent: 'center', '& > *': { gridRow: 1, diff --git a/packages/tldraw/src/components/IconButton/index.ts b/packages/tldraw/src/components/IconButton/index.ts new file mode 100644 index 000000000..a37a7fc4a --- /dev/null +++ b/packages/tldraw/src/components/IconButton/index.ts @@ -0,0 +1 @@ +export * from './IconButton' diff --git a/packages/tldraw/src/components/shared/kbd.tsx b/packages/tldraw/src/components/Kbd/Kbd.tsx similarity index 85% rename from packages/tldraw/src/components/shared/kbd.tsx rename to packages/tldraw/src/components/Kbd/Kbd.tsx index 311cf1baa..b24cb954b 100644 --- a/packages/tldraw/src/components/shared/kbd.tsx +++ b/packages/tldraw/src/components/Kbd/Kbd.tsx @@ -1,14 +1,12 @@ import * as React from 'react' -import css from '~styles' +import styled from '~styles' import { Utils } from '@tldraw/core' /* -------------------------------------------------- */ /* Keyboard Shortcut */ /* -------------------------------------------------- */ -export function commandKey(): string { - return Utils.isDarwin() ? '⌘' : 'Ctrl' -} +const commandKey = () => (Utils.isDarwin() ? '⌘' : 'Ctrl') export function Kbd({ variant, @@ -18,18 +16,18 @@ export function Kbd({ children: string }): JSX.Element | null { return ( - + {children .replaceAll('#', commandKey()) .split('') .map((k, i) => ( {k} ))} - + ) } -export const kbd = css({ +export const StyledKbd = styled('kbd', { marginLeft: '$3', textShadow: '$2', textAlign: 'center', diff --git a/packages/tldraw/src/components/Kbd/index.ts b/packages/tldraw/src/components/Kbd/index.ts new file mode 100644 index 000000000..f9fae9026 --- /dev/null +++ b/packages/tldraw/src/components/Kbd/index.ts @@ -0,0 +1 @@ +export * from './Kbd' diff --git a/packages/tldraw/src/components/MenuContent/MenuContent.ts b/packages/tldraw/src/components/MenuContent/MenuContent.ts new file mode 100644 index 000000000..3130108bb --- /dev/null +++ b/packages/tldraw/src/components/MenuContent/MenuContent.ts @@ -0,0 +1,17 @@ +import styled from '~styles' + +export const MenuContent = styled('div', { + position: 'relative', + overflow: 'hidden', + userSelect: 'none', + display: 'flex', + flexDirection: 'column', + zIndex: 180, + minWidth: 180, + pointerEvents: 'all', + backgroundColor: '$panel', + boxShadow: '$panel', + padding: '$2 $2', + borderRadius: '$3', + font: '$ui', +}) diff --git a/packages/tldraw/src/components/MenuContent/index.ts b/packages/tldraw/src/components/MenuContent/index.ts new file mode 100644 index 000000000..b83f44f2e --- /dev/null +++ b/packages/tldraw/src/components/MenuContent/index.ts @@ -0,0 +1 @@ +export * from './MenuContent' diff --git a/packages/tldraw/src/components/Panel/Panel.tsx b/packages/tldraw/src/components/Panel/Panel.tsx new file mode 100644 index 000000000..154bcfdfb --- /dev/null +++ b/packages/tldraw/src/components/Panel/Panel.tsx @@ -0,0 +1,30 @@ +import styled from '~styles/stitches.config' + +export const Panel = styled('div', { + backgroundColor: '$panel', + display: 'flex', + flexDirection: 'row', + padding: '0 $2', + boxShadow: '$panel', + variants: { + side: { + center: { + borderTopLeftRadius: '$4', + borderTopRightRadius: '$4', + // borderTop: '1px solid $panelBorder', + // borderLeft: '1px solid $panelBorder', + // borderRight: '1px solid $panelBorder', + }, + left: { + borderBottomRightRadius: '$4', + // borderBottom: '1px solid $panelBorder', + // borderRight: '1px solid $panelBorder', + }, + right: { + borderBottomLeftRadius: '$4', + // borderBottom: '1px solid $panelBorder', + // borderLeft: '1px solid $panelBorder', + }, + }, + }, +}) diff --git a/packages/tldraw/src/components/Panel/index.ts b/packages/tldraw/src/components/Panel/index.ts new file mode 100644 index 000000000..303f56392 --- /dev/null +++ b/packages/tldraw/src/components/Panel/index.ts @@ -0,0 +1 @@ +export * from './Panel' diff --git a/packages/tldraw/src/components/RowButton/RowButton.tsx b/packages/tldraw/src/components/RowButton/RowButton.tsx new file mode 100644 index 000000000..cadfa7a76 --- /dev/null +++ b/packages/tldraw/src/components/RowButton/RowButton.tsx @@ -0,0 +1,142 @@ +import { ItemIndicator } from '@radix-ui/react-dropdown-menu' +import { ChevronRightIcon, CheckIcon } from '@radix-ui/react-icons' +import * as React from 'react' +import { breakpoints } from '~components/breakpoints' +import { Kbd } from '~components/Kbd' +import { SmallIcon } from '~components/SmallIcon' +import styled from '~styles' + +export interface RowButtonProps { + onSelect?: () => void + children: React.ReactNode + disabled?: boolean + kbd?: string + isActive?: boolean + isWarning?: boolean + hasIndicator?: boolean + hasArrow?: boolean +} + +export const RowButton = React.forwardRef( + ( + { + onSelect, + isActive = false, + isWarning = false, + hasIndicator = false, + hasArrow = false, + disabled = false, + kbd, + children, + ...rest + }, + ref + ) => { + return ( + + + {children} + {kbd ? {kbd} : undefined} + {hasIndicator && ( + + + + + + )} + {hasArrow && ( + + + + )} + + + ) + } +) + +const StyledRowButtonInner = styled('div', { + height: '100%', + width: '100%', + color: '$text', + fontFamily: '$ui', + fontWeight: 400, + fontSize: '$1', + backgroundColor: '$panel', + borderRadius: '$2', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + padding: '0 $3', + justifyContent: 'space-between', + border: '1px solid transparent', + + '& svg': { + position: 'relative', + stroke: '$overlay', + strokeWidth: 1, + zIndex: 1, + }, +}) + +export const StyledRowButton = styled('button', { + position: 'relative', + width: '100%', + background: 'none', + border: 'none', + cursor: 'pointer', + height: '32px', + outline: 'none', + borderRadius: 4, + userSelect: 'none', + margin: 0, + padding: '0 0', + + '&[data-disabled]': { + opacity: 0.3, + }, + + '&:disabled': { + opacity: 0.3, + }, + + variants: { + bp: { + mobile: {}, + small: {}, + }, + size: { + icon: { + padding: '4px ', + width: 'auto', + }, + }, + isWarning: { + true: { + color: '$warn', + }, + }, + isActive: { + true: { + backgroundColor: '$hover', + }, + false: { + [`&:hover:not(:disabled) ${StyledRowButtonInner}`]: { + backgroundColor: '$hover', + border: '1px solid $panel', + '& *[data-shy="true"]': { + opacity: 1, + }, + }, + }, + }, + }, +}) diff --git a/packages/tldraw/src/components/RowButton/index.ts b/packages/tldraw/src/components/RowButton/index.ts new file mode 100644 index 000000000..5bcc7ae0f --- /dev/null +++ b/packages/tldraw/src/components/RowButton/index.ts @@ -0,0 +1 @@ +export * from './RowButton' diff --git a/packages/tldraw/src/components/SmallIcon/SmallIcon.tsx b/packages/tldraw/src/components/SmallIcon/SmallIcon.tsx new file mode 100644 index 000000000..6aee4a78d --- /dev/null +++ b/packages/tldraw/src/components/SmallIcon/SmallIcon.tsx @@ -0,0 +1,27 @@ +import styled from '~styles' + +export const SmallIcon = styled('div', { + height: '100%', + borderRadius: '4px', + marginRight: '1px', + width: 'fit-content', + display: 'grid', + alignItems: 'center', + justifyContent: 'center', + outline: 'none', + border: 'none', + pointerEvents: 'all', + cursor: 'pointer', + color: '$text', + + '& svg': { + height: 16, + width: 16, + strokeWidth: 1, + }, + + '& > *': { + gridRow: 1, + gridColumn: 1, + }, +}) diff --git a/packages/tldraw/src/components/SmallIcon/index.ts b/packages/tldraw/src/components/SmallIcon/index.ts new file mode 100644 index 000000000..3f7098ffa --- /dev/null +++ b/packages/tldraw/src/components/SmallIcon/index.ts @@ -0,0 +1 @@ +export * from './SmallIcon' diff --git a/packages/tldraw/src/components/ToolButton/ToolButton.tsx b/packages/tldraw/src/components/ToolButton/ToolButton.tsx new file mode 100644 index 000000000..3229f64ba --- /dev/null +++ b/packages/tldraw/src/components/ToolButton/ToolButton.tsx @@ -0,0 +1,132 @@ +import * as React from 'react' +import { Tooltip } from '~components/Tooltip' +import styled from '~styles' + +export interface ToolButtonProps { + onSelect?: () => void + onDoubleClick?: () => void + isActive?: boolean + variant?: 'icon' | 'text' | 'circle' | 'primary' + children: React.ReactNode +} + +export const ToolButton = React.forwardRef( + ({ onSelect, onDoubleClick, isActive = false, variant, children, ...rest }, ref) => { + return ( + + + {children} + + + ) + } +) + +/* ------------------ With Tooltip ------------------ */ + +interface ToolButtonWithTooltipProps extends ToolButtonProps { + label: string + kbd?: string +} + +export function ToolButtonWithTooltip({ label, kbd, ...rest }: ToolButtonWithTooltipProps) { + return ( + + + + ) +} + +export const StyledToolButtonInner = styled('div', { + position: 'relative', + height: '100%', + width: '100%', + color: '$text', + backgroundColor: '$panel', + borderRadius: '$2', + margin: '0', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: '$ui', + userSelect: 'none', + boxSizing: 'border-box', + border: '1px solid transparent', + + variants: { + variant: { + primary: { + '& svg': { + width: 20, + height: 20, + }, + }, + icon: { + display: 'grid', + '& > *': { + gridRow: 1, + gridColumn: 1, + }, + }, + text: { + fontSize: '$1', + padding: '0 $3', + }, + circle: { + borderRadius: '100%', + boxShadow: '$panel', + }, + }, + isActive: { + true: { + backgroundColor: '$selected', + color: '$panelActive', + }, + }, + }, +}) + +export const StyledToolButton = styled('button', { + position: 'relative', + color: '$text', + height: '48px', + width: '40px', + fontSize: '$0', + background: 'none', + margin: '0', + padding: '$3 $2', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + outline: 'none', + cursor: 'pointer', + pointerEvents: 'all', + border: 'none', + + variants: { + variant: { + primary: {}, + icon: {}, + text: { + width: 'auto', + }, + circle: {}, + }, + isActive: { + true: {}, + false: { + [`&:hover:not(:disabled) ${StyledToolButtonInner}`]: { + backgroundColor: '$hover', + border: '1px solid $panel', + }, + }, + }, + }, +}) diff --git a/packages/tldraw/src/components/ToolButton/index.ts b/packages/tldraw/src/components/ToolButton/index.ts new file mode 100644 index 000000000..7f7d067a6 --- /dev/null +++ b/packages/tldraw/src/components/ToolButton/index.ts @@ -0,0 +1 @@ +export * from './ToolButton' diff --git a/packages/tldraw/src/components/ToolsPanel/ActionButton.tsx b/packages/tldraw/src/components/ToolsPanel/ActionButton.tsx new file mode 100644 index 000000000..d60bf2b0f --- /dev/null +++ b/packages/tldraw/src/components/ToolsPanel/ActionButton.tsx @@ -0,0 +1,289 @@ +import * as React from 'react' +import { Tooltip } from '~components/Tooltip/Tooltip' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { useTLDrawContext } from '~hooks' +import styled from '~styles' +import { AlignType, Data, DistributeType, StretchType } from '~types' +import { + ArrowDownIcon, + ArrowUpIcon, + AspectRatioIcon, + CopyIcon, + DotsHorizontalIcon, + GroupIcon, + LockClosedIcon, + LockOpen1Icon, + PinBottomIcon, + PinTopIcon, + RotateCounterClockwiseIcon, + AlignBottomIcon, + AlignCenterHorizontallyIcon, + AlignCenterVerticallyIcon, + AlignLeftIcon, + AlignRightIcon, + AlignTopIcon, + SpaceEvenlyHorizontallyIcon, + SpaceEvenlyVerticallyIcon, + StretchHorizontallyIcon, + StretchVerticallyIcon, +} from '@radix-ui/react-icons' +import { DMContent } from '~components/DropdownMenu' +import { Divider } from '~components/Divider' +import { TrashIcon } from '~components/icons' +import { IconButton } from '~components/IconButton' +import { ToolButton } from '~components/ToolButton' + +const selectedShapesCountSelector = (s: Data) => + s.document.pageStates[s.appState.currentPageId].selectedIds.length + +const isAllLockedSelector = (s: Data) => { + const page = s.document.pages[s.appState.currentPageId] + const { selectedIds } = s.document.pageStates[s.appState.currentPageId] + return selectedIds.every((id) => page.shapes[id].isLocked) +} + +const isAllAspectLockedSelector = (s: Data) => { + const page = s.document.pages[s.appState.currentPageId] + const { selectedIds } = s.document.pageStates[s.appState.currentPageId] + return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked) +} + +const isAllGroupedSelector = (s: Data) => { + const page = s.document.pages[s.appState.currentPageId] + const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map( + (id) => page.shapes[id] + ) + + return selectedShapes.every( + (shape) => + shape.children !== undefined || + (shape.parentId === selectedShapes[0].parentId && + selectedShapes[0].parentId !== s.appState.currentPageId) + ) +} + +const hasSelectionSelector = (s: Data) => { + const { selectedIds } = s.document.pageStates[s.appState.currentPageId] + return selectedIds.length > 0 +} + +const hasMultipleSelectionSelector = (s: Data) => { + const { selectedIds } = s.document.pageStates[s.appState.currentPageId] + return selectedIds.length > 1 +} + +export function ActionButton(): JSX.Element { + const { tlstate, useSelector } = useTLDrawContext() + + const isAllLocked = useSelector(isAllLockedSelector) + + const isAllAspectLocked = useSelector(isAllAspectLockedSelector) + + const isAllGrouped = useSelector(isAllGroupedSelector) + + const hasSelection = useSelector(hasSelectionSelector) + + const hasMultipleSelection = useSelector(hasMultipleSelectionSelector) + + const handleRotate = React.useCallback(() => { + tlstate.rotate() + }, [tlstate]) + + const handleDuplicate = React.useCallback(() => { + tlstate.duplicate() + }, [tlstate]) + + const handleToggleLocked = React.useCallback(() => { + tlstate.toggleLocked() + }, [tlstate]) + + const handleToggleAspectRatio = React.useCallback(() => { + tlstate.toggleAspectRatioLocked() + }, [tlstate]) + + const handleGroup = React.useCallback(() => { + tlstate.group() + }, [tlstate]) + + const handleMoveToBack = React.useCallback(() => { + tlstate.moveToBack() + }, [tlstate]) + + const handleMoveBackward = React.useCallback(() => { + tlstate.moveBackward() + }, [tlstate]) + + const handleMoveForward = React.useCallback(() => { + tlstate.moveForward() + }, [tlstate]) + + const handleMoveToFront = React.useCallback(() => { + tlstate.moveToFront() + }, [tlstate]) + + const handleDelete = React.useCallback(() => { + tlstate.delete() + }, [tlstate]) + + const alignTop = React.useCallback(() => { + tlstate.align(AlignType.Top) + }, [tlstate]) + + const alignCenterVertical = React.useCallback(() => { + tlstate.align(AlignType.CenterVertical) + }, [tlstate]) + + const alignBottom = React.useCallback(() => { + tlstate.align(AlignType.Bottom) + }, [tlstate]) + + const stretchVertically = React.useCallback(() => { + tlstate.stretch(StretchType.Vertical) + }, [tlstate]) + + const distributeVertically = React.useCallback(() => { + tlstate.distribute(DistributeType.Vertical) + }, [tlstate]) + + const alignLeft = React.useCallback(() => { + tlstate.align(AlignType.Left) + }, [tlstate]) + + const alignCenterHorizontal = React.useCallback(() => { + tlstate.align(AlignType.CenterHorizontal) + }, [tlstate]) + + const alignRight = React.useCallback(() => { + tlstate.align(AlignType.Right) + }, [tlstate]) + + const stretchHorizontally = React.useCallback(() => { + tlstate.stretch(StretchType.Horizontal) + }, [tlstate]) + + const distributeHorizontally = React.useCallback(() => { + tlstate.distribute(DistributeType.Horizontal) + }, [tlstate]) + + const selectedShapesCount = useSelector(selectedShapesCountSelector) + + const hasTwoOrMore = selectedShapesCount > 1 + const hasThreeOrMore = selectedShapesCount > 2 + + return ( + + + + + + + + <> + + + + + + + + + + + + + + {isAllLocked ? : } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export const ButtonsRow = styled('div', { + position: 'relative', + display: 'flex', + width: '100%', + background: 'none', + border: 'none', + cursor: 'pointer', + outline: 'none', + alignItems: 'center', + justifyContent: 'flex-start', + padding: 0, +}) diff --git a/packages/tldraw/src/components/tools-panel/back-to-content/back-to-content.tsx b/packages/tldraw/src/components/ToolsPanel/BackToContent.tsx similarity index 63% rename from packages/tldraw/src/components/tools-panel/back-to-content/back-to-content.tsx rename to packages/tldraw/src/components/ToolsPanel/BackToContent.tsx index 215835be9..ac1224e33 100644 --- a/packages/tldraw/src/components/tools-panel/back-to-content/back-to-content.tsx +++ b/packages/tldraw/src/components/ToolsPanel/BackToContent.tsx @@ -1,8 +1,9 @@ import * as React from 'react' -import { floatingContainer, rowButton } from '~components/shared' -import css from '~styles' +import styled from '~styles' import type { Data } from '~types' import { useTLDrawContext } from '~hooks' +import { RowButton } from '~components/RowButton' +import { MenuContent } from '~components/MenuContent' const isEmptyCanvasSelector = (s: Data) => Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 && @@ -16,17 +17,16 @@ export const BackToContent = React.memo(() => { if (!isEmptyCanvas) return null return ( -
- -
+ + Back to content + ) }) -const backToContentButton = css(floatingContainer, { +const BackToContentContainer = styled(MenuContent, { pointerEvents: 'all', width: 'fit-content', + minWidth: 0, gridRow: 1, flexGrow: 2, display: 'block', diff --git a/packages/tldraw/src/components/ToolsPanel/LockButton.tsx b/packages/tldraw/src/components/ToolsPanel/LockButton.tsx new file mode 100644 index 000000000..f387c80fe --- /dev/null +++ b/packages/tldraw/src/components/ToolsPanel/LockButton.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons' +import { Tooltip } from '~components/Tooltip' +import { useTLDrawContext } from '~hooks' +import { ToolButton } from '~components/ToolButton' +import type { Data } from '~types' + +const isToolLockedSelector = (s: Data) => s.appState.isToolLocked + +export function LockButton(): JSX.Element { + const { tlstate, useSelector } = useTLDrawContext() + + const isToolLocked = useSelector(isToolLockedSelector) + + return ( + + + {isToolLocked ? : } + + + ) +} diff --git a/packages/tldraw/src/components/tools-panel/primary-tools/primary-tools.tsx b/packages/tldraw/src/components/ToolsPanel/PrimaryTools.tsx similarity index 66% rename from packages/tldraw/src/components/tools-panel/primary-tools/primary-tools.tsx rename to packages/tldraw/src/components/ToolsPanel/PrimaryTools.tsx index d7f7f68f9..7e4182449 100644 --- a/packages/tldraw/src/components/tools-panel/primary-tools/primary-tools.tsx +++ b/packages/tldraw/src/components/ToolsPanel/PrimaryTools.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { ArrowTopRightIcon, CircleIcon, + CursorArrowIcon, Pencil1Icon, Pencil2Icon, SquareIcon, @@ -9,8 +10,8 @@ import { } from '@radix-ui/react-icons' import { Data, TLDrawShapeType } from '~types' import { useTLDrawContext } from '~hooks' -import { floatingContainer } from '~components/shared' -import { PrimaryButton } from '~components/tools-panel/styled' +import { ToolButtonWithTooltip } from '~components/ToolButton' +import { Panel } from '~components/Panel' const activeToolSelector = (s: Data) => s.appState.activeTool @@ -19,6 +20,10 @@ export const PrimaryTools = React.memo((): JSX.Element => { const activeTool = useSelector(activeToolSelector) + const selectSelectTool = React.useCallback(() => { + tlstate.selectTool('select') + }, [tlstate]) + const selectDrawTool = React.useCallback(() => { tlstate.selectTool(TLDrawShapeType.Draw) }, [tlstate]) @@ -44,55 +49,63 @@ export const PrimaryTools = React.memo((): JSX.Element => { }, [tlstate]) return ( -
- + + + + - - + - - + - - + - - + - - + - -
+ + ) }) diff --git a/packages/tldraw/src/components/tools-panel/status-bar/status-bar.tsx b/packages/tldraw/src/components/ToolsPanel/StatusBar.tsx similarity index 76% rename from packages/tldraw/src/components/tools-panel/status-bar/status-bar.tsx rename to packages/tldraw/src/components/ToolsPanel/StatusBar.tsx index 7a490f706..c60557978 100644 --- a/packages/tldraw/src/components/tools-panel/status-bar/status-bar.tsx +++ b/packages/tldraw/src/components/ToolsPanel/StatusBar.tsx @@ -1,7 +1,8 @@ import * as React from 'react' import { useTLDrawContext } from '~hooks' import type { Data } from '~types' -import css from '~styles' +import styled from '~styles' +import { breakpoints } from '~components/breakpoints' const statusSelector = (s: Data) => s.appState.status const activeToolSelector = (s: Data) => s.appState.activeTool @@ -12,15 +13,15 @@ export function StatusBar(): JSX.Element | null { const activeTool = useSelector(activeToolSelector) return ( -
-
+ + {activeTool} | {status} -
-
+ + ) } -const statusBarContainer = css({ +const StyledStatusBar = styled('div', { height: 40, userSelect: 'none', borderTop: '1px solid $border', @@ -36,7 +37,7 @@ const statusBarContainer = css({ padding: '0 16px', variants: { - size: { + bp: { small: { fontSize: '$1', }, @@ -44,7 +45,7 @@ const statusBarContainer = css({ }, }) -const section = css({ +const StyledSection = styled('div', { whiteSpace: 'nowrap', overflow: 'hidden', }) diff --git a/packages/tldraw/src/components/tools-panel/tools-panel.test.tsx b/packages/tldraw/src/components/ToolsPanel/ToolsPanel.test.tsx similarity index 82% rename from packages/tldraw/src/components/tools-panel/tools-panel.test.tsx rename to packages/tldraw/src/components/ToolsPanel/ToolsPanel.test.tsx index 12ba8d9f6..fce5e771a 100644 --- a/packages/tldraw/src/components/tools-panel/tools-panel.test.tsx +++ b/packages/tldraw/src/components/ToolsPanel/ToolsPanel.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { ToolsPanel } from './tools-panel' +import { ToolsPanel } from './ToolsPanel' import { renderWithContext } from '~test' describe('tools panel', () => { diff --git a/packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx b/packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx new file mode 100644 index 000000000..ddf00887c --- /dev/null +++ b/packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx @@ -0,0 +1,78 @@ +import * as React from 'react' +import styled from '~styles' +import type { Data } from '~types' +import { useTLDrawContext } from '~hooks' +import { StatusBar } from './StatusBar' +import { BackToContent } from './BackToContent' +import { PrimaryTools } from './PrimaryTools' +import { ActionButton } from './ActionButton' +import { LockButton } from './LockButton' + +const isDebugModeSelector = (s: Data) => s.settings.isDebugMode + +export const ToolsPanel = React.memo((): JSX.Element => { + const { useSelector } = useTLDrawContext() + + const isDebugMode = useSelector(isDebugModeSelector) + + return ( + + + + + + + + + + {isDebugMode && ( + + + + )} + + ) +}) + +const StyledToolsPanelContainer = styled('div', { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + width: '100%', + minWidth: 0, + maxWidth: '100%', + display: 'grid', + gridTemplateColumns: 'auto auto auto', + gridTemplateRows: 'auto auto', + justifyContent: 'space-between', + padding: '0', + alignItems: 'flex-end', + zIndex: 200, + pointerEvents: 'none', + '& > div > *': { + pointerEvents: 'all', + }, +}) + +const StyledCenterWrap = styled('div', { + gridRow: 1, + gridColumn: 2, + display: 'flex', + width: 'fit-content', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + gap: 12, +}) + +const StyledStatusWrap = styled('div', { + gridRow: 2, + gridColumn: '1 / span 3', +}) + +const StyledPrimaryTools = styled('div', { + position: 'relative', + display: 'flex', + gap: '$2', +}) diff --git a/packages/tldraw/src/components/ToolsPanel/index.ts b/packages/tldraw/src/components/ToolsPanel/index.ts new file mode 100644 index 000000000..78bb05019 --- /dev/null +++ b/packages/tldraw/src/components/ToolsPanel/index.ts @@ -0,0 +1 @@ +export * from './ToolsPanel' diff --git a/packages/tldraw/src/components/shared/tooltip.tsx b/packages/tldraw/src/components/Tooltip/Tooltip.tsx similarity index 75% rename from packages/tldraw/src/components/shared/tooltip.tsx rename to packages/tldraw/src/components/Tooltip/Tooltip.tsx index 677bd2178..7d1aa4693 100644 --- a/packages/tldraw/src/components/shared/tooltip.tsx +++ b/packages/tldraw/src/components/Tooltip/Tooltip.tsx @@ -1,7 +1,7 @@ import * as RadixTooltip from '@radix-ui/react-tooltip' import * as React from 'react' -import css from '~styles' -import { Kbd } from './kbd' +import { Kbd } from '~components/Kbd' +import styled from '~styles' /* -------------------------------------------------- */ /* Tooltip */ @@ -25,22 +25,16 @@ export function Tooltip({ {children} - + {label} {kbdProp ? {kbdProp} : null} - - + + ) } -const button = css({ - border: 'none', - background: 'none', - padding: 0, -}) - -const content = css({ +const StyledContent = styled(RadixTooltip.Content, { borderRadius: 3, padding: '$3 $3 $3 $3', fontSize: '$1', @@ -53,7 +47,7 @@ const content = css({ userSelect: 'none', }) -const arrow = css({ +const StyledArrow = styled(RadixTooltip.Arrow, { fill: '$tooltipBg', margin: '0 8px', }) diff --git a/packages/tldraw/src/components/Tooltip/index.ts b/packages/tldraw/src/components/Tooltip/index.ts new file mode 100644 index 000000000..ba15f4073 --- /dev/null +++ b/packages/tldraw/src/components/Tooltip/index.ts @@ -0,0 +1 @@ +export * from './Tooltip' diff --git a/packages/tldraw/src/components/TopPanel/ColorMenu.tsx b/packages/tldraw/src/components/TopPanel/ColorMenu.tsx new file mode 100644 index 000000000..3387cabd2 --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/ColorMenu.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { strokes } from '~shape-utils' +import { useTheme, useTLDrawContext } from '~hooks' +import type { Data, ColorStyle } from '~types' +import CircleIcon from '~components/icons/CircleIcon' +import { DMContent, DMRadioItem, DMTriggerIcon } from '~components/DropdownMenu' +import { BoxIcon } from '~components/icons' +import { IconButton } from '~components/IconButton' +import { ToolButton } from '~components/ToolButton' +import { Tooltip } from '~components/Tooltip' + +const selectColor = (s: Data) => s.appState.selectedStyle.color + +export const ColorMenu = React.memo((): JSX.Element => { + const { theme } = useTheme() + const { tlstate, useSelector } = useTLDrawContext() + + const color = useSelector(selectColor) + + return ( + + + + + + {Object.keys(strokes[theme]).map((colorStyle: string) => ( + tlstate.style({ color: colorStyle as ColorStyle })} + > + + + ))} + + + ) +}) diff --git a/packages/tldraw/src/components/TopPanel/DashMenu.tsx b/packages/tldraw/src/components/TopPanel/DashMenu.tsx new file mode 100644 index 000000000..e1863f49e --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/DashMenu.tsx @@ -0,0 +1,100 @@ +import * as React from 'react' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { useTLDrawContext } from '~hooks' +import { DashStyle, Data } from '~types' +import { DMContent, DMRadioItem, DMTriggerIcon } from '~components/DropdownMenu' +import { ToolButton } from '~components/ToolButton' +import { Tooltip } from '~components/Tooltip' + +const dashes = { + [DashStyle.Draw]: , + [DashStyle.Solid]: , + [DashStyle.Dashed]: , + [DashStyle.Dotted]: , +} + +const selectDash = (s: Data) => s.appState.selectedStyle.dash + +export const DashMenu = React.memo((): JSX.Element => { + const { tlstate, useSelector } = useTLDrawContext() + + const dash = useSelector(selectDash) + + return ( + + {dashes[dash]} + + {Object.keys(DashStyle).map((dashStyle) => ( + tlstate.style({ dash: dashStyle as DashStyle })} + > + {dashes[dashStyle as DashStyle]} + + ))} + + + ) +}) + +function DashSolidIcon(): JSX.Element { + return ( + + + + ) +} + +function DashDashedIcon(): JSX.Element { + return ( + + + + ) +} + +const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}` + +function DashDottedIcon(): JSX.Element { + return ( + + + + ) +} + +function DashDrawIcon(): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/TopPanel/FillCheckbox.tsx b/packages/tldraw/src/components/TopPanel/FillCheckbox.tsx new file mode 100644 index 000000000..a3d07c75e --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/FillCheckbox.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' +import * as Checkbox from '@radix-ui/react-checkbox' +import { useTLDrawContext } from '~hooks' +import type { Data } from '~types' +import { Tooltip } from '~components/Tooltip' +import { BoxIcon, IsFilledIcon } from '~components/icons' +import { ToolButton } from '~components/ToolButton' + +const isFilledSelector = (s: Data) => s.appState.selectedStyle.isFilled + +export const FillCheckbox = React.memo((): JSX.Element => { + const { tlstate, useSelector } = useTLDrawContext() + + const isFilled = useSelector(isFilledSelector) + + const handleIsFilledChange = React.useCallback( + (isFilled: boolean) => tlstate.style({ isFilled }), + [tlstate] + ) + + return ( + + + + + + + + + ) +}) diff --git a/packages/tldraw/src/components/TopPanel/Menu.tsx b/packages/tldraw/src/components/TopPanel/Menu.tsx new file mode 100644 index 000000000..ad0e4c8cc --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/Menu.tsx @@ -0,0 +1,111 @@ +import * as React from 'react' +import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { useTLDrawContext } from '~hooks' +import { PreferencesMenu } from './PreferencesMenu' +import { DMItem, DMContent, DMDivider, DMSubMenu, DMTriggerIcon } from '~components/DropdownMenu' +import { SmallIcon } from '~components/SmallIcon' + +export const Menu = React.memo(() => { + const { tlstate } = useTLDrawContext() + + const handleNew = React.useCallback(() => { + if (window.confirm('Are you sure you want to start a new project?')) { + tlstate.newProject() + } + }, [tlstate]) + + const handleSave = React.useCallback(() => { + tlstate.saveProject() + }, [tlstate]) + + const handleLoad = React.useCallback(() => { + tlstate.loadProject() + }, [tlstate]) + + const handleSignOut = React.useCallback(() => { + tlstate.signOut() + }, [tlstate]) + + const handleCopy = React.useCallback(() => { + tlstate.copy() + }, [tlstate]) + + const handlePaste = React.useCallback(() => { + tlstate.paste() + }, [tlstate]) + + const handleCopySvg = React.useCallback(() => { + tlstate.copySvg() + }, [tlstate]) + + const handleCopyJson = React.useCallback(() => { + tlstate.copyJson() + }, [tlstate]) + + const handleSelectAll = React.useCallback(() => { + tlstate.selectAll() + }, [tlstate]) + + const handleDeselectAll = React.useCallback(() => { + tlstate.deselectAll() + }, [tlstate]) + + return ( + + + + + + + + New Project + + + Open... + + + Save + + + Save As... + + + + + Undo + + + Redo + + + + Copy + + + Paste + + + + Copy as SVG + + Copy as JSON + + + Select All + + Select None + + + + + + Sign Out + + + + + + + ) +}) diff --git a/packages/tldraw/src/components/page-panel/page-panel.tsx b/packages/tldraw/src/components/TopPanel/PageMenu.tsx similarity index 62% rename from packages/tldraw/src/components/page-panel/page-panel.tsx rename to packages/tldraw/src/components/TopPanel/PageMenu.tsx index de2f49343..63168c68d 100644 --- a/packages/tldraw/src/components/page-panel/page-panel.tsx +++ b/packages/tldraw/src/components/TopPanel/PageMenu.tsx @@ -1,19 +1,14 @@ import * as React from 'react' import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import { PlusIcon, CheckIcon } from '@radix-ui/react-icons' -import { - breakpoints, - DropdownMenuButton, - DropdownMenuDivider, - rowButton, - menuContent, - floatingContainer, - iconWrapper, -} from '~components/shared' -import { PageOptionsDialog } from '~components/page-options-dialog' -import css from '~styles' +import { PageOptionsDialog } from './PageOptionsDialog' +import styled from '~styles' import { useTLDrawContext } from '~hooks' import type { Data } from '~types' +import { DMContent, DMDivider } from '~components/DropdownMenu' +import { SmallIcon } from '~components/SmallIcon' +import { RowButton } from '~components/RowButton' +import { ToolButton } from '~components/ToolButton' const sortedSelector = (s: Data) => 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 const currentPageIdSelector = (s: Data) => s.document.pages[s.appState.currentPageId].id -export function PagePanel(): JSX.Element { +export function PageMenu(): JSX.Element { const { useSelector } = useTLDrawContext() const rIsOpen = React.useRef(false) @@ -51,14 +46,12 @@ export function PagePanel(): JSX.Element { return ( -
- - {currentPageName || 'Page'} - -
- + + {currentPageName || 'Page'} + + {isOpen && } - +
) } @@ -86,34 +79,40 @@ function PageMenuContent({ onClose }: { onClose: () => void }) { <> {sortedPages.map((page) => ( -
+ - {page.name || 'Page'} - -
- -
-
+ + {page.name || 'Page'} + + + + + +
-
+ ))}
- - - Create Page -
- -
-
+ + + + Create Page + + + + + ) } -const buttonWithOptions = css({ +const ButtonWithOptions = styled('div', { display: 'grid', gridTemplateColumns: '1fr auto', gridAutoFlow: 'column', @@ -126,3 +125,7 @@ const buttonWithOptions = css({ opacity: 1, }, }) + +export const PageButton = styled(RowButton, { + minWidth: 128, +}) diff --git a/packages/tldraw/src/components/TopPanel/PageOptionsDialog.tsx b/packages/tldraw/src/components/TopPanel/PageOptionsDialog.tsx new file mode 100644 index 000000000..efe858e32 --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/PageOptionsDialog.tsx @@ -0,0 +1,139 @@ +import * as React from 'react' +import * as Dialog from '@radix-ui/react-alert-dialog' +import { MixerVerticalIcon } from '@radix-ui/react-icons' +import type { Data, TLDrawPage } from '~types' +import { useTLDrawContext } from '~hooks' +import { RowButton, RowButtonProps } from '~components/RowButton' +import styled from '~styles' +import { Divider } from '~components/Divider' +import { IconButton } from '~components/IconButton/IconButton' +import { SmallIcon } from '~components/SmallIcon' +import { breakpoints } from '~components/breakpoints' + +const canDeleteSelector = (s: Data) => { + return Object.keys(s.document.pages).length > 1 +} + +interface PageOptionsDialogProps { + page: TLDrawPage + onOpen?: () => void + onClose?: () => void +} + +export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element { + const { tlstate, useSelector } = useTLDrawContext() + + const [isOpen, setIsOpen] = React.useState(false) + + const canDelete = useSelector(canDeleteSelector) + + const rInput = React.useRef(null) + + const handleDuplicate = React.useCallback(() => { + tlstate.duplicatePage(page.id) + onClose?.() + }, [tlstate]) + + const handleDelete = React.useCallback(() => { + if (window.confirm(`Are you sure you want to delete this page?`)) { + tlstate.deletePage(page.id) + onClose?.() + } + }, [tlstate]) + + const handleOpenChange = React.useCallback( + (isOpen: boolean) => { + setIsOpen(isOpen) + + if (isOpen) { + onOpen?.() + return + } + }, + [tlstate, name] + ) + + function stopPropagation(e: React.KeyboardEvent) { + e.stopPropagation() + } + + // TODO: Replace with text input + function handleRename() { + const nextName = window.prompt('New name:', page.name) + tlstate.renamePage(page.id, nextName || page.name || 'Page') + } + + React.useEffect(() => { + if (isOpen) { + requestAnimationFrame(() => { + rInput.current?.focus() + rInput.current?.select() + }) + } + }, [isOpen]) + + return ( + + + + + + + + + + + Rename + Duplicate + + Delete + + + + Cancel + + + + ) +} + +/* -------------------------------------------------- */ +/* Dialog */ +/* -------------------------------------------------- */ + +export const StyledDialogContent = styled(Dialog.Content, { + position: 'fixed', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + minWidth: 240, + maxWidth: 'fit-content', + maxHeight: '85vh', + marginTop: '-5vh', + pointerEvents: 'all', + backgroundColor: '$panel', + border: '1px solid $panelBorder', + padding: '$0', + borderRadius: '$2', + font: '$ui', + '&:focus': { + outline: 'none', + }, +}) + +export const StyledDialogOverlay = styled(Dialog.Overlay, { + backgroundColor: 'rgba(0, 0, 0, .15)', + position: 'fixed', + top: 0, + right: 0, + bottom: 0, + left: 0, +}) + +function DialogAction({ onSelect, ...rest }: RowButtonProps) { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx b/packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx new file mode 100644 index 000000000..e9de8294b --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' +import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/DropdownMenu' +import { useTLDrawContext } from '~hooks' +import type { Data } from '~types' + +const settingsSelector = (s: Data) => s.settings + +export function PreferencesMenu() { + const { tlstate, useSelector } = useTLDrawContext() + + const settings = useSelector(settingsSelector) + + const toggleDebugMode = React.useCallback(() => { + tlstate.setSetting('isDebugMode', (v) => !v) + }, [tlstate]) + + const toggleDarkMode = React.useCallback(() => { + tlstate.setSetting('isDarkMode', (v) => !v) + }, [tlstate]) + + const toggleFocusMode = React.useCallback(() => { + tlstate.setSetting('isFocusMode', (v) => !v) + }, [tlstate]) + + const toggleRotateHandle = React.useCallback(() => { + tlstate.setSetting('showRotateHandles', (v) => !v) + }, [tlstate]) + + const toggleBoundShapesHandle = React.useCallback(() => { + tlstate.setSetting('showBindingHandles', (v) => !v) + }, [tlstate]) + + const toggleisSnapping = React.useCallback(() => { + tlstate.setSetting('isSnapping', (v) => !v) + }, [tlstate]) + + const toggleCloneControls = React.useCallback(() => { + tlstate.setSetting('showCloneHandles', (v) => !v) + }, [tlstate]) + + return ( + + + Dark Mode + + + Focus Mode + + + Debug Mode + + + + Rotate Handles + + + Binding Handles + + + Clone Handles + + + Always Show Snaps + + + ) +} diff --git a/packages/tldraw/src/components/TopPanel/SizeMenu.tsx b/packages/tldraw/src/components/TopPanel/SizeMenu.tsx new file mode 100644 index 000000000..a34f766fd --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/SizeMenu.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { Data, SizeStyle } from '~types' +import { useTLDrawContext } from '~hooks' +import { DMContent, DMTriggerIcon } from '~components/DropdownMenu' +import { ToolButton } from '~components/ToolButton' +import { SizeSmallIcon, SizeMediumIcon, SizeLargeIcon } from '~components/icons' +import { Tooltip } from '~components/Tooltip' + +const sizes = { + [SizeStyle.Small]: , + [SizeStyle.Medium]: , + [SizeStyle.Large]: , +} + +const selectSize = (s: Data) => s.appState.selectedStyle.size + +export const SizeMenu = React.memo((): JSX.Element => { + const { tlstate, useSelector } = useTLDrawContext() + + const size = useSelector(selectSize) + + return ( + + {sizes[size as SizeStyle]} + + {Object.keys(SizeStyle).map((sizeStyle: string) => ( + tlstate.style({ size: sizeStyle as SizeStyle })} + > + {sizes[sizeStyle as SizeStyle]} + + ))} + + + ) +}) diff --git a/packages/tldraw/src/components/TopPanel/TopPanel.tsx b/packages/tldraw/src/components/TopPanel/TopPanel.tsx new file mode 100644 index 000000000..0b5942e5e --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/TopPanel.tsx @@ -0,0 +1,58 @@ +import * as React from 'react' +import { Menu } from './Menu' +import styled from '~styles' +import { PageMenu } from './PageMenu' +import { ZoomMenu } from './ZoomMenu' +import { DashMenu } from './DashMenu' +import { SizeMenu } from './SizeMenu' +import { FillCheckbox } from './FillCheckbox' +import { ColorMenu } from './ColorMenu' +import { Panel } from '~components/Panel' + +interface TopPanelProps { + showPages: boolean + showMenu: boolean + showStyles: boolean + showZoom: boolean +} + +export function TopPanel({ showPages, showMenu, showStyles, showZoom }: TopPanelProps) { + return ( + + {(showMenu || showPages) && ( + + {showMenu && } + {showPages && } + + )} + + {(showStyles || showZoom) && ( + + {showStyles && ( + <> + + + + + + )} + {showZoom && } + + )} + + ) +} + +const StyledTopPanel = styled('div', { + width: '100%', + position: 'absolute', + top: 0, + left: 0, + right: 0, + display: 'flex', + flexDirection: 'row', +}) + +const StyledSpacer = styled('div', { + flexGrow: 2, +}) diff --git a/packages/tldraw/src/components/TopPanel/ZoomMenu.tsx b/packages/tldraw/src/components/TopPanel/ZoomMenu.tsx new file mode 100644 index 000000000..c2520d6e5 --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/ZoomMenu.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' +import { useTLDrawContext } from '~hooks' +import type { Data } from '~types' +import styled from '~styles' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { DMItem, DMContent } from '~components/DropdownMenu' +import { ToolButton } from '~components/ToolButton' + +const zoomSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId].camera.zoom + +export function ZoomMenu() { + const { tlstate, useSelector } = useTLDrawContext() + const zoom = useSelector(zoomSelector) + + return ( + + + {Math.round(zoom * 100)}% + + + + Zoom In + + + Zoom Out + + + To 100% + + + To Fit + + + To Selection + + + + ) +} + +const FixedWidthToolButton = styled(ToolButton, { + minWidth: 56, +}) diff --git a/packages/tldraw/src/components/TopPanel/index.ts b/packages/tldraw/src/components/TopPanel/index.ts new file mode 100644 index 000000000..acfe58700 --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/index.ts @@ -0,0 +1 @@ +export * from './TopPanel' diff --git a/packages/tldraw/src/components/shared/breakpoints.tsx b/packages/tldraw/src/components/breakpoints.tsx similarity index 100% rename from packages/tldraw/src/components/shared/breakpoints.tsx rename to packages/tldraw/src/components/breakpoints.tsx diff --git a/packages/tldraw/src/components/context-menu/context-menu.tsx b/packages/tldraw/src/components/context-menu/context-menu.tsx deleted file mode 100644 index 69f3d5420..000000000 --- a/packages/tldraw/src/components/context-menu/context-menu.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import * as React from 'react' -import css from '~styles' -import * as RadixContextMenu from '@radix-ui/react-context-menu' -import { useTLDrawContext } from '~hooks' -import { Data, AlignType, DistributeType, StretchType } from '~types' -import { - Kbd, - iconWrapper, - breakpoints, - rowButton, - ContextMenuArrow, - ContextMenuDivider, - ContextMenuButton, - ContextMenuSubMenu, - ContextMenuIconButton, - ContextMenuRoot, - menuContent, -} from '../shared' -import { - ChevronRightIcon, - AlignBottomIcon, - AlignCenterHorizontallyIcon, - AlignCenterVerticallyIcon, - AlignLeftIcon, - AlignRightIcon, - AlignTopIcon, - SpaceEvenlyHorizontallyIcon, - SpaceEvenlyVerticallyIcon, - StretchHorizontallyIcon, - StretchVerticallyIcon, -} from '@radix-ui/react-icons' - -const has1SelectedIdsSelector = (s: Data) => { - return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0 -} -const has2SelectedIdsSelector = (s: Data) => { - return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1 -} -const has3SelectedIdsSelector = (s: Data) => { - return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2 -} - -const isDebugModeSelector = (s: Data) => { - return s.settings.isDebugMode -} - -const hasGroupSelectedSelector = (s: Data) => { - return s.document.pageStates[s.appState.currentPageId].selectedIds.some( - (id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined - ) -} - -interface ContextMenuProps { - children: React.ReactNode -} - -export const ContextMenu = ({ children }: ContextMenuProps): JSX.Element => { - const { tlstate, useSelector } = useTLDrawContext() - const hasSelection = useSelector(has1SelectedIdsSelector) - const hasTwoOrMore = useSelector(has2SelectedIdsSelector) - const hasThreeOrMore = useSelector(has3SelectedIdsSelector) - const isDebugMode = useSelector(isDebugModeSelector) - const hasGroupSelected = useSelector(hasGroupSelectedSelector) - - const rContent = React.useRef(null) - - const handleFlipHorizontal = React.useCallback(() => { - tlstate.flipHorizontal() - }, [tlstate]) - - const handleFlipVertical = React.useCallback(() => { - tlstate.flipVertical() - }, [tlstate]) - - const handleDuplicate = React.useCallback(() => { - tlstate.duplicate() - }, [tlstate]) - - const handleGroup = React.useCallback(() => { - tlstate.group() - }, [tlstate]) - - const handleMoveToBack = React.useCallback(() => { - tlstate.moveToBack() - }, [tlstate]) - - const handleMoveBackward = React.useCallback(() => { - tlstate.moveBackward() - }, [tlstate]) - - const handleMoveForward = React.useCallback(() => { - tlstate.moveForward() - }, [tlstate]) - - const handleMoveToFront = React.useCallback(() => { - tlstate.moveToFront() - }, [tlstate]) - - const handleDelete = React.useCallback(() => { - tlstate.delete() - }, [tlstate]) - - const handleCopyJson = React.useCallback(() => { - tlstate.copyJson() - }, [tlstate]) - - const handleCopy = React.useCallback(() => { - tlstate.copy() - }, [tlstate]) - - const handlePaste = React.useCallback(() => { - tlstate.paste() - }, [tlstate]) - - const handleCopySvg = React.useCallback(() => { - tlstate.copySvg() - }, [tlstate]) - - const handleUndo = React.useCallback(() => { - tlstate.undo() - }, [tlstate]) - - const handleRedo = React.useCallback(() => { - tlstate.redo() - }, [tlstate]) - - return ( - - {children} - - {hasSelection ? ( - <> - - Flip Horizontal - ⇧H - - - Flip Vertical - ⇧V - - - Duplicate - #D - - - {hasGroupSelected || - (hasTwoOrMore && ( - <> - {hasGroupSelected && ( - - Ungroup - #⇧G - - )} - {hasTwoOrMore && ( - - Group - #G - - )} - - ))} - - - To Front - ⇧] - - - Forward - ] - - - Backward - [ - - - To Back - ⇧[ - - - - {hasTwoOrMore && ( - - )} - - - Copy - #C - - - Copy to SVG - ⇧#C - - {isDebugMode && ( - - Copy to JSON - - )} - - Paste - #V - - - - Delete - ⌫ - - - ) : ( - <> - - Paste - #V - - - Undo - #Z - - - Redo - #⇧Z - - - )} - - - ) -} - -function AlignDistributeSubMenu({ - hasThreeOrMore, -}: { - hasTwoOrMore: boolean - hasThreeOrMore: boolean -}) { - const { tlstate } = useTLDrawContext() - - const alignTop = React.useCallback(() => { - tlstate.align(AlignType.Top) - }, [tlstate]) - - const alignCenterVertical = React.useCallback(() => { - tlstate.align(AlignType.CenterVertical) - }, [tlstate]) - - const alignBottom = React.useCallback(() => { - tlstate.align(AlignType.Bottom) - }, [tlstate]) - - const stretchVertically = React.useCallback(() => { - tlstate.stretch(StretchType.Vertical) - }, [tlstate]) - - const distributeVertically = React.useCallback(() => { - tlstate.distribute(DistributeType.Vertical) - }, [tlstate]) - - const alignLeft = React.useCallback(() => { - tlstate.align(AlignType.Left) - }, [tlstate]) - - const alignCenterHorizontal = React.useCallback(() => { - tlstate.align(AlignType.CenterHorizontal) - }, [tlstate]) - - const alignRight = React.useCallback(() => { - tlstate.align(AlignType.Right) - }, [tlstate]) - - const stretchHorizontally = React.useCallback(() => { - tlstate.stretch(StretchType.Horizontal) - }, [tlstate]) - - const distributeHorizontally = React.useCallback(() => { - tlstate.distribute(DistributeType.Horizontal) - }, [tlstate]) - - return ( - - - Align / Distribute -
- -
-
- - - - - - - - - - - - - - {hasThreeOrMore && ( - - - - )} - - - - - - - - - - - - - {hasThreeOrMore && ( - - - - )} - - -
- ) -} - -const grid = css(menuContent, { - display: 'grid', - variants: { - selectedStyle: { - threeOrMore: { - gridTemplateColumns: 'repeat(5, auto)', - }, - twoOrMore: { - gridTemplateColumns: 'repeat(4, auto)', - }, - }, - }, -}) - -const currentPageIdSelector = (s: Data) => s.appState.currentPageId -const documentPagesSelector = (s: Data) => s.document.pages - -function MoveToPageMenu(): JSX.Element | null { - const { tlstate, useSelector } = useTLDrawContext() - const currentPageId = useSelector(currentPageIdSelector) - const documentPages = useSelector(documentPagesSelector) - - const sorted = Object.values(documentPages) - .sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0)) - .filter((a) => a.id !== currentPageId) - - if (sorted.length === 0) return null - - return ( - - - Move To Page -
- -
-
- - {sorted.map(({ id, name }, i) => ( - tlstate.moveToPage(id)} - > - {name || `Page ${i}`} - - ))} - - -
- ) -} diff --git a/packages/tldraw/src/components/context-menu/index.ts b/packages/tldraw/src/components/context-menu/index.ts deleted file mode 100644 index beff13a99..000000000 --- a/packages/tldraw/src/components/context-menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './context-menu' diff --git a/packages/tldraw/src/components/icons/BoxIcon.tsx b/packages/tldraw/src/components/icons/BoxIcon.tsx new file mode 100644 index 000000000..4a3937d0f --- /dev/null +++ b/packages/tldraw/src/components/icons/BoxIcon.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' + +export function BoxIcon({ + fill = 'none', + stroke = 'currentColor', +}: { + fill?: string + stroke?: string +}): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/check.tsx b/packages/tldraw/src/components/icons/CheckIcon.tsx similarity index 98% rename from packages/tldraw/src/components/icons/check.tsx rename to packages/tldraw/src/components/icons/CheckIcon.tsx index 563acdfdd..19ee800e3 100644 --- a/packages/tldraw/src/components/icons/check.tsx +++ b/packages/tldraw/src/components/icons/CheckIcon.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -function SvgCheck(props: React.SVGProps): JSX.Element { +function CheckIcon(props: React.SVGProps): JSX.Element { return ( ): JSX.Element { ) } -export default SvgCheck +export default CheckIcon diff --git a/packages/tldraw/src/components/icons/circle.tsx b/packages/tldraw/src/components/icons/CircleIcon.tsx similarity index 100% rename from packages/tldraw/src/components/icons/circle.tsx rename to packages/tldraw/src/components/icons/CircleIcon.tsx diff --git a/packages/tldraw/src/components/icons/DashDashedIcon.tsx b/packages/tldraw/src/components/icons/DashDashedIcon.tsx new file mode 100644 index 000000000..14332057b --- /dev/null +++ b/packages/tldraw/src/components/icons/DashDashedIcon.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' + +export function DashDashedIcon(): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/DashDottedIcon.tsx b/packages/tldraw/src/components/icons/DashDottedIcon.tsx new file mode 100644 index 000000000..f95793aca --- /dev/null +++ b/packages/tldraw/src/components/icons/DashDottedIcon.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' + +const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}` + +export function DashDottedIcon(): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/DashDrawIcon.tsx b/packages/tldraw/src/components/icons/DashDrawIcon.tsx new file mode 100644 index 000000000..85ded7ed6 --- /dev/null +++ b/packages/tldraw/src/components/icons/DashDrawIcon.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' + +export function DashDrawIcon(): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/DashSolidIcon.tsx b/packages/tldraw/src/components/icons/DashSolidIcon.tsx new file mode 100644 index 000000000..38acf27c6 --- /dev/null +++ b/packages/tldraw/src/components/icons/DashSolidIcon.tsx @@ -0,0 +1,9 @@ +import * as React from 'react' + +export function DashSolidIcon(): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/IsFilledIcon.tsx b/packages/tldraw/src/components/icons/IsFilledIcon.tsx new file mode 100644 index 000000000..9efd8429d --- /dev/null +++ b/packages/tldraw/src/components/icons/IsFilledIcon.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' + +export function IsFilledIcon(): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/redo.tsx b/packages/tldraw/src/components/icons/RedoIcon.tsx similarity index 70% rename from packages/tldraw/src/components/icons/redo.tsx rename to packages/tldraw/src/components/icons/RedoIcon.tsx index ee8320df2..516c8972b 100644 --- a/packages/tldraw/src/components/icons/redo.tsx +++ b/packages/tldraw/src/components/icons/RedoIcon.tsx @@ -1,8 +1,8 @@ import * as React from 'react' -function SvgRedo(props: React.SVGProps): JSX.Element { +export function RedoIcon(props: React.SVGProps): JSX.Element { return ( - + ): JSX.Element { ) } - -export default SvgRedo diff --git a/packages/tldraw/src/components/icons/SizeLargeIcon.tsx b/packages/tldraw/src/components/icons/SizeLargeIcon.tsx new file mode 100644 index 000000000..151eddf2a --- /dev/null +++ b/packages/tldraw/src/components/icons/SizeLargeIcon.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' + +export function SizeLargeIcon(props: React.SVGProps): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/SizeMediumIcon.tsx b/packages/tldraw/src/components/icons/SizeMediumIcon.tsx new file mode 100644 index 000000000..ae9bf8778 --- /dev/null +++ b/packages/tldraw/src/components/icons/SizeMediumIcon.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' + +export function SizeMediumIcon(props: React.SVGProps): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/SizeSmallIcon.tsx b/packages/tldraw/src/components/icons/SizeSmallIcon.tsx new file mode 100644 index 000000000..864787d2f --- /dev/null +++ b/packages/tldraw/src/components/icons/SizeSmallIcon.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' + +export function SizeSmallIcon(props: React.SVGProps): JSX.Element { + return ( + + + + ) +} diff --git a/packages/tldraw/src/components/icons/trash.tsx b/packages/tldraw/src/components/icons/TrashIcon.tsx similarity index 90% rename from packages/tldraw/src/components/icons/trash.tsx rename to packages/tldraw/src/components/icons/TrashIcon.tsx index 229412ff6..8eaa9fc56 100644 --- a/packages/tldraw/src/components/icons/trash.tsx +++ b/packages/tldraw/src/components/icons/TrashIcon.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -function SvgTrash(props: React.SVGProps): JSX.Element { +export function TrashIcon(props: React.SVGProps): JSX.Element { return ( ): JSX.Element { ) } - -export default SvgTrash diff --git a/packages/tldraw/src/components/icons/undo.tsx b/packages/tldraw/src/components/icons/UndoIcon.tsx similarity index 70% rename from packages/tldraw/src/components/icons/undo.tsx rename to packages/tldraw/src/components/icons/UndoIcon.tsx index caf816582..d6215a413 100644 --- a/packages/tldraw/src/components/icons/undo.tsx +++ b/packages/tldraw/src/components/icons/UndoIcon.tsx @@ -1,8 +1,8 @@ import * as React from 'react' -function SvgUndo(props: React.SVGProps): JSX.Element { +export function UndoIcon(props: React.SVGProps): JSX.Element { return ( - + ): JSX.Element { ) } - -export default SvgUndo diff --git a/packages/tldraw/src/components/icons/index.ts b/packages/tldraw/src/components/icons/index.ts new file mode 100644 index 000000000..7702dcbe1 --- /dev/null +++ b/packages/tldraw/src/components/icons/index.ts @@ -0,0 +1,14 @@ +export * from './BoxIcon' +export * from './CheckIcon' +export * from './CircleIcon' +export * from './DashDashedIcon' +export * from './DashDottedIcon' +export * from './DashDrawIcon' +export * from './DashSolidIcon' +export * from './IsFilledIcon' +export * from './RedoIcon' +export * from './TrashIcon' +export * from './UndoIcon' +export * from './SizeSmallIcon' +export * from './SizeMediumIcon' +export * from './SizeLargeIcon' diff --git a/packages/tldraw/src/components/icons/index.tsx b/packages/tldraw/src/components/icons/index.tsx deleted file mode 100644 index 9da9315bf..000000000 --- a/packages/tldraw/src/components/icons/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export { default as Redo } from './redo' -export { default as Trash } from './trash' -export { default as Undo } from './undo' -export { default as Check } from './check' -export { default as CircleIcon } from './circle' diff --git a/packages/tldraw/src/components/index.ts b/packages/tldraw/src/components/index.ts deleted file mode 100644 index 66f3fc9eb..000000000 --- a/packages/tldraw/src/components/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './shared/tooltip' -export * from './shared/kbd' -export * from './shared' -export * from './icons' -export * from './tldraw' diff --git a/packages/tldraw/src/components/menu/index.ts b/packages/tldraw/src/components/menu/index.ts deleted file mode 100644 index 45467d073..000000000 --- a/packages/tldraw/src/components/menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './menu' diff --git a/packages/tldraw/src/components/menu/menu.test.tsx b/packages/tldraw/src/components/menu/menu.test.tsx deleted file mode 100644 index e0ac975b3..000000000 --- a/packages/tldraw/src/components/menu/menu.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from 'react' -import { Menu } from './menu' -import { renderWithContext } from '~test' - -describe('menu', () => { - test('mounts component without crashing', () => { - renderWithContext() - }) -}) diff --git a/packages/tldraw/src/components/menu/menu.tsx b/packages/tldraw/src/components/menu/menu.tsx deleted file mode 100644 index 392a5a224..000000000 --- a/packages/tldraw/src/components/menu/menu.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import * as React from 'react' -import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { - floatingContainer, - DropdownMenuRoot, - menuContent, - iconButton, - breakpoints, - DropdownMenuButton, - DropdownMenuSubMenu, - DropdownMenuDivider, - iconWrapper, - Kbd, -} from '~components/shared' -import { useTLDrawContext } from '~hooks' -import { Preferences } from './preferences' - -export const Menu = React.memo(() => { - const { tlstate } = useTLDrawContext() - - const handleNew = React.useCallback(() => { - if (window.confirm('Are you sure you want to start a new project?')) { - tlstate.newProject() - } - }, [tlstate]) - - const handleSave = React.useCallback(() => { - tlstate.saveProject() - }, [tlstate]) - - const handleLoad = React.useCallback(() => { - tlstate.loadProject() - }, [tlstate]) - - const handleSignOut = React.useCallback(() => { - tlstate.signOut() - }, [tlstate]) - - return ( -
- - - - - - - New Project - #N - - - - Open... - #L - - - - - Save - #S - - - Save As... - ⇧#S - - - - - - Sign Out -
- -
-
-
-
-
- ) -}) - -function RecentFiles() { - return ( - - - Project A - - - Project B - - - Project C - - - ) -} diff --git a/packages/tldraw/src/components/menu/preferences.tsx b/packages/tldraw/src/components/menu/preferences.tsx deleted file mode 100644 index a2b1b8fc8..000000000 --- a/packages/tldraw/src/components/menu/preferences.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from 'react' -import { - DropdownMenuDivider, - DropdownMenuSubMenu, - DropdownMenuCheckboxItem, - Kbd, -} from '~components/shared' -import { useTLDrawContext } from '~hooks' -import type { Data } from '~types' - -const settingsSelector = (s: Data) => s.settings - -export function Preferences() { - const { tlstate, useSelector } = useTLDrawContext() - - const settings = useSelector(settingsSelector) - - const toggleDebugMode = React.useCallback(() => { - tlstate.setSetting('isDebugMode', (v) => !v) - }, [tlstate]) - - const toggleDarkMode = React.useCallback(() => { - tlstate.setSetting('isDarkMode', (v) => !v) - }, [tlstate]) - - const toggleFocusMode = React.useCallback(() => { - tlstate.setSetting('isFocusMode', (v) => !v) - }, [tlstate]) - - const toggleRotateHandle = React.useCallback(() => { - tlstate.setSetting('showRotateHandles', (v) => !v) - }, [tlstate]) - - const toggleBoundShapesHandle = React.useCallback(() => { - tlstate.setSetting('showBindingHandles', (v) => !v) - }, [tlstate]) - - const toggleisSnapping = React.useCallback(() => { - tlstate.setSetting('isSnapping', (v) => !v) - }, [tlstate]) - - const toggleCloneControls = React.useCallback(() => { - tlstate.setSetting('showCloneHandles', (v) => !v) - }, [tlstate]) - - return ( - - - Dark Mode - #⇧D - - - Focus Mode - ⇧. - - - Debug Mode - - - - Rotate Handles - - - Binding Handles - - - Clone Handles - - - Always Show Snaps - - - ) -} diff --git a/packages/tldraw/src/components/page-options-dialog/index.ts b/packages/tldraw/src/components/page-options-dialog/index.ts deleted file mode 100644 index 2d13bdb3b..000000000 --- a/packages/tldraw/src/components/page-options-dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './page-options-dialog' diff --git a/packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx b/packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx deleted file mode 100644 index f9ceb7e19..000000000 --- a/packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from 'react' -import { PageOptionsDialog } from './page-options-dialog' -import { mockDocument, renderWithContext } from '~test' - -describe('page options dialog', () => { - test('mounts component without crashing', () => { - renderWithContext() - }) -}) diff --git a/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx b/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx deleted file mode 100644 index 12a9db124..000000000 --- a/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import * as React from 'react' -import * as Dialog from '@radix-ui/react-alert-dialog' -import { MixerVerticalIcon } from '@radix-ui/react-icons' -import { - breakpoints, - iconButton, - dialogOverlay, - dialogContent, - rowButton, - divider, -} from '~components/shared' -import type { Data, TLDrawPage } from '~types' -import { useTLDrawContext } from '~hooks' - -const canDeleteSelector = (s: Data) => { - return Object.keys(s.document.pages).length > 1 -} - -interface PageOptionsDialogProps { - page: TLDrawPage - onOpen?: () => void - onClose?: () => void -} - -export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element { - const { tlstate, useSelector } = useTLDrawContext() - - const [isOpen, setIsOpen] = React.useState(false) - - const canDelete = useSelector(canDeleteSelector) - - const rInput = React.useRef(null) - - const handleDuplicate = React.useCallback(() => { - tlstate.duplicatePage(page.id) - onClose?.() - }, [tlstate]) - - const handleDelete = React.useCallback(() => { - if (window.confirm(`Are you sure you want to delete this page?`)) { - tlstate.deletePage(page.id) - onClose?.() - } - }, [tlstate]) - - const handleOpenChange = React.useCallback( - (isOpen: boolean) => { - setIsOpen(isOpen) - - if (isOpen) { - onOpen?.() - return - } - }, - [tlstate, name] - ) - - function stopPropagation(e: React.KeyboardEvent) { - e.stopPropagation() - } - - // TODO: Replace with text input - function handleRename() { - const nextName = window.prompt('New name:', page.name) - tlstate.renamePage(page.id, nextName || page.name || 'Page') - } - - React.useEffect(() => { - if (isOpen) { - requestAnimationFrame(() => { - rInput.current?.focus() - rInput.current?.select() - }) - } - }, [isOpen]) - - return ( - - - - - - - - Rename - - - Duplicate - - - Delete - -
- Cancel - - - ) -} diff --git a/packages/tldraw/src/components/page-panel/index.ts b/packages/tldraw/src/components/page-panel/index.ts deleted file mode 100644 index 333268ef3..000000000 --- a/packages/tldraw/src/components/page-panel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './page-panel' diff --git a/packages/tldraw/src/components/page-panel/page-panel.test.tsx b/packages/tldraw/src/components/page-panel/page-panel.test.tsx deleted file mode 100644 index bb577e494..000000000 --- a/packages/tldraw/src/components/page-panel/page-panel.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from 'react' -import { PagePanel } from './page-panel' -import { renderWithContext } from '~test' - -describe('page panel', () => { - test('mounts component without crashing', () => { - renderWithContext() - }) -}) diff --git a/packages/tldraw/src/components/shared/buttons-row.tsx b/packages/tldraw/src/components/shared/buttons-row.tsx deleted file mode 100644 index a49433191..000000000 --- a/packages/tldraw/src/components/shared/buttons-row.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import css from '~styles' - -/* -------------------------------------------------- */ -/* Buttons Row */ -/* -------------------------------------------------- */ - -export const buttonsRow = css({ - position: 'relative', - display: 'flex', - width: '100%', - background: 'none', - border: 'none', - cursor: 'pointer', - outline: 'none', - alignItems: 'center', - justifyContent: 'flex-start', - padding: 0, -}) diff --git a/packages/tldraw/src/components/shared/context-menu.tsx b/packages/tldraw/src/components/shared/context-menu.tsx deleted file mode 100644 index b014c2ee2..000000000 --- a/packages/tldraw/src/components/shared/context-menu.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import * as React from 'react' -import { CheckIcon, ChevronRightIcon } from '@radix-ui/react-icons' -import { - Root as CMRoot, - TriggerItem as CMTriggerItem, - Separator as CMSeparator, - Item as CMItem, - Arrow as CMArrow, - Content as CMContent, - ItemIndicator as CMItemIndicator, - CheckboxItem as CMCheckboxItem, -} from '@radix-ui/react-context-menu' -import { breakpoints } from './breakpoints' -import { rowButton } from './row-button' -import { iconButton } from './icon-button' -import { iconWrapper } from './icon-wrapper' -import { menuContent } from './menu' -import css from '~styles' - -/* -------------------------------------------------- */ -/* Context Menu */ -/* -------------------------------------------------- */ - -export interface ContextMenuRootProps { - onOpenChange?: (isOpen: boolean) => void - children: React.ReactNode -} - -export function ContextMenuRoot({ onOpenChange, children }: ContextMenuRootProps): JSX.Element { - return ( - - {children} - - ) -} - -export interface ContextMenuSubMenuProps { - label: string - children: React.ReactNode -} - -export function ContextMenuSubMenu({ children, label }: ContextMenuSubMenuProps): JSX.Element { - return ( - - - {label} -
- -
-
- - {children} - - -
- ) -} - -const contextMenuDivider = css({ - backgroundColor: '$hover', - height: 1, - margin: '$2 -$2', -}) - -export const ContextMenuDivider = React.forwardRef< - React.ElementRef, - React.ComponentProps ->((props, forwardedRef) => ( - -)) - -const contextMenuArrow = css({ - fill: '$panel', -}) - -export const ContextMenuArrow = React.forwardRef< - React.ElementRef, - React.ComponentProps ->((props, forwardedRef) => ( - -)) - -export interface ContextMenuButtonProps { - onSelect?: () => void - disabled?: boolean - children: React.ReactNode -} - -export function ContextMenuButton({ - onSelect, - children, - disabled = false, -}: ContextMenuButtonProps): JSX.Element { - return ( - - {children} - - ) -} - -interface ContextMenuIconButtonProps { - onSelect: () => void - disabled?: boolean - children: React.ReactNode -} - -export function ContextMenuIconButton({ - onSelect, - children, - disabled = false, -}: ContextMenuIconButtonProps): JSX.Element { - return ( - - {children} - - ) -} - -interface ContextMenuCheckboxItemProps { - checked: boolean - disabled?: boolean - onCheckedChange: (isChecked: boolean) => void - children: React.ReactNode -} - -export function ContextMenuCheckboxItem({ - checked, - disabled = false, - onCheckedChange, - children, -}: ContextMenuCheckboxItemProps): JSX.Element { - return ( - - {children} - -
- -
-
-
- ) -} diff --git a/packages/tldraw/src/components/shared/dialog.tsx b/packages/tldraw/src/components/shared/dialog.tsx deleted file mode 100644 index 10d3d1ec2..000000000 --- a/packages/tldraw/src/components/shared/dialog.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import css from '~styles' - -/* -------------------------------------------------- */ -/* Dialog */ -/* -------------------------------------------------- */ - -export const dialogContent = css({ - position: 'fixed', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - minWidth: 240, - maxWidth: 'fit-content', - maxHeight: '85vh', - marginTop: '-5vh', - pointerEvents: 'all', - backgroundColor: '$panel', - border: '1px solid $panel', - padding: '$0', - boxShadow: '$4', - borderRadius: '4px', - font: '$ui', - - '&:focus': { - outline: 'none', - }, -}) - -export const dialogOverlay = css({ - backgroundColor: 'rgba(0, 0, 0, .15)', - position: 'fixed', - top: 0, - right: 0, - bottom: 0, - left: 0, -}) - -export const dialogInputWrapper = css({ - padding: '$4 $2', -}) - -export const dialogTitleRow = css({ - display: 'flex', - padding: '0 0 0 $4', - alignItems: 'center', - justifyContent: 'space-between', - - h3: { - fontSize: '$1', - }, -}) diff --git a/packages/tldraw/src/components/shared/dropdown-menu.tsx b/packages/tldraw/src/components/shared/dropdown-menu.tsx deleted file mode 100644 index 2fdc8faa8..000000000 --- a/packages/tldraw/src/components/shared/dropdown-menu.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import * as React from 'react' -import { CheckIcon, ChevronRightIcon } from '@radix-ui/react-icons' -import { - Root as DMRoot, - TriggerItem as DMTriggerItem, - Separator as DMSeparator, - Item as DMItem, - Arrow as DMArrow, - Content as DMContent, - Trigger as DMTrigger, - ItemIndicator as DMItemIndicator, - CheckboxItem as DMCheckboxItem, -} from '@radix-ui/react-dropdown-menu' - -import { Tooltip } from './tooltip' -import { breakpoints } from './breakpoints' -import { rowButton } from './row-button' -import { iconButton } from './icon-button' -import { iconWrapper } from './icon-wrapper' -import { menuContent } from './menu' - -import css from '~styles' - -/* -------------------------------------------------- */ -/* Dropdown Menu */ -/* -------------------------------------------------- */ - -export interface DropdownMenuRootProps { - isOpen?: boolean - onOpenChange?: (isOpen: boolean) => void - children: React.ReactNode -} - -export function DropdownMenuRoot({ - isOpen, - onOpenChange, - children, -}: DropdownMenuRootProps): JSX.Element { - return ( - - {children} - - ) -} - -export interface DropdownMenuSubMenuProps { - label: string - disabled?: boolean - children: React.ReactNode -} - -export function DropdownMenuSubMenu({ - children, - disabled = false, - label, -}: DropdownMenuSubMenuProps): JSX.Element { - return ( - - - {label} -
- -
-
- - {children} - - -
- ) -} - -export const dropdownMenuDivider = css({ - backgroundColor: '$hover', - height: 1, - marginTop: '$2', - marginRight: '-$2', - marginBottom: '$2', - marginLeft: '-$2', -}) - -export const DropdownMenuDivider = React.forwardRef< - React.ElementRef, - React.ComponentProps ->((props, forwardedRef) => ( - -)) - -export const dropdownMenuArrow = css({ - fill: '$panel', -}) - -export const DropdownMenuArrow = React.forwardRef< - React.ElementRef, - React.ComponentProps ->((props, forwardedRef) => ( - -)) - -export interface DropdownMenuButtonProps { - onSelect?: () => void - disabled?: boolean - children: React.ReactNode -} - -export function DropdownMenuButton({ - onSelect, - children, - disabled = false, -}: DropdownMenuButtonProps): JSX.Element { - return ( - - {children} - - ) -} - -interface DropdownMenuIconButtonProps { - onSelect: () => void - disabled?: boolean - children: React.ReactNode -} - -export function DropdownMenuIconButton({ - onSelect, - children, - disabled = false, -}: DropdownMenuIconButtonProps): JSX.Element { - return ( - - {children} - - ) -} - -interface DropdownMenuIconTriggerButtonProps { - label: string - kbd?: string - disabled?: boolean - children: React.ReactNode -} - -export function DropdownMenuIconTriggerButton({ - label, - kbd, - children, - disabled = false, -}: DropdownMenuIconTriggerButtonProps): JSX.Element { - return ( - - - {children} - - - ) -} - -interface MenuCheckboxItemProps { - checked: boolean - disabled?: boolean - onCheckedChange: (isChecked: boolean) => void - children: React.ReactNode -} - -export function DropdownMenuCheckboxItem({ - checked, - disabled = false, - onCheckedChange, - children, -}: MenuCheckboxItemProps): JSX.Element { - return ( - - {children} - -
- -
-
-
- ) -} diff --git a/packages/tldraw/src/components/shared/floating-container.tsx b/packages/tldraw/src/components/shared/floating-container.tsx deleted file mode 100644 index cdc611708..000000000 --- a/packages/tldraw/src/components/shared/floating-container.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import css from '~styles' - -/* -------------------------------------------------- */ -/* Floating Container */ -/* -------------------------------------------------- */ - -export const floatingContainer = css({ - backgroundColor: '$panel', - willChange: 'transform', - border: '1px solid $panel', - borderRadius: '4px', - boxShadow: '$4', - display: 'flex', - height: 'fit-content', - padding: '$0', - pointerEvents: 'all', - position: 'relative', - userSelect: 'none', - zIndex: 200, - variants: { - direction: { - row: { - flexDirection: 'row', - }, - column: { - flexDirection: 'column', - }, - }, - elevation: { - 0: { - boxShadow: 'none', - }, - 2: { - boxShadow: '$2', - }, - 3: { - boxShadow: '$3', - }, - 4: { - boxShadow: '$4', - }, - }, - }, -}) diff --git a/packages/tldraw/src/components/shared/icon-wrapper.tsx b/packages/tldraw/src/components/shared/icon-wrapper.tsx deleted file mode 100644 index 089aeb1cc..000000000 --- a/packages/tldraw/src/components/shared/icon-wrapper.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import css from '~styles' - -/* -------------------------------------------------- */ -/* Icon Wrapper */ -/* -------------------------------------------------- */ - -export const iconWrapper = css({ - height: '100%', - borderRadius: '4px', - marginRight: '1px', - display: 'grid', - alignItems: 'center', - justifyContent: 'center', - outline: 'none', - border: 'none', - pointerEvents: 'all', - cursor: 'pointer', - color: '$text', - - '& svg': { - height: 22, - width: 22, - strokeWidth: 1, - }, - - '& > *': { - gridRow: 1, - gridColumn: 1, - }, - - variants: { - size: { - small: { - '& svg': { - height: '16px', - width: '16px', - }, - }, - medium: { - '& svg': { - height: '22px', - width: '22px', - }, - }, - }, - }, -}) diff --git a/packages/tldraw/src/components/shared/index.ts b/packages/tldraw/src/components/shared/index.ts deleted file mode 100644 index 64b34a6b9..000000000 --- a/packages/tldraw/src/components/shared/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './breakpoints' -export * from './buttons-row' -export * from './context-menu' -export * from './dialog' -export * from './dropdown-menu' -export * from './floating-container' -export * from './icon-button' -export * from './icon-wrapper' -export * from './kbd' -export * from './menu' -export * from './radio-group' -export * from './row-button' -export * from './tooltip' diff --git a/packages/tldraw/src/components/shared/menu.tsx b/packages/tldraw/src/components/shared/menu.tsx deleted file mode 100644 index 70d9b1621..000000000 --- a/packages/tldraw/src/components/shared/menu.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { breakpoints } from './breakpoints' -import css from '~styles' -import { rowButton } from './row-button' - -/* -------------------------------------------------- */ -/* Menu */ -/* -------------------------------------------------- */ - -export const menuContent = css({ - position: 'relative', - overflow: 'hidden', - userSelect: 'none', - zIndex: 180, - minWidth: 180, - pointerEvents: 'all', - backgroundColor: '$panel', - border: '1px solid $panel', - padding: '$0', - boxShadow: '$4', - borderRadius: '4px', - font: '$ui', -}) - -export const divider = css({ - backgroundColor: '$hover', - height: 1, - marginTop: '$2', - marginRight: '-$2', - marginBottom: '$2', - marginLeft: '-$2', -}) - -export function MenuButton({ - warn, - onSelect, - children, - disabled = false, -}: { - warn?: boolean - onSelect?: () => void - disabled?: boolean - children: React.ReactNode -}): JSX.Element { - return ( - - ) -} - -export const menuTextInput = css({ - backgroundColor: '$panel', - border: 'none', - padding: '$4 $3', - width: '100%', - outline: 'none', - background: '$input', - borderRadius: '4px', - fontFamily: '$ui', - fontSize: '$1', - userSelect: 'all', -}) diff --git a/packages/tldraw/src/components/shared/radio-group.tsx b/packages/tldraw/src/components/shared/radio-group.tsx deleted file mode 100644 index 2f0021675..000000000 --- a/packages/tldraw/src/components/shared/radio-group.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import css from '~styles' -import { Root as RGRoot } from '@radix-ui/react-radio-group' - -/* -------------------------------------------------- */ -/* Radio Group */ -/* -------------------------------------------------- */ - -export const group = css({ - display: 'flex', -}) - -export const Group = React.forwardRef< - React.ElementRef, - React.ComponentProps ->((props, forwardedRef) => ( - -)) diff --git a/packages/tldraw/src/components/shared/row-button.tsx b/packages/tldraw/src/components/shared/row-button.tsx deleted file mode 100644 index 60ae5bc73..000000000 --- a/packages/tldraw/src/components/shared/row-button.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import css from '~styles' - -/* -------------------------------------------------- */ -/* Row Button */ -/* -------------------------------------------------- */ - -export const rowButton = css({ - position: 'relative', - display: 'flex', - width: '100%', - background: 'none', - height: '32px', - border: 'none', - cursor: 'pointer', - color: '$text', - outline: 'none', - alignItems: 'center', - fontFamily: '$ui', - fontWeight: 400, - fontSize: '$1', - justifyContent: 'space-between', - padding: '4px 8px 4px 12px', - borderRadius: 4, - userSelect: 'none', - - '& label': { - fontWeight: '$1', - margin: 0, - padding: 0, - }, - - '& svg': { - position: 'relative', - stroke: '$overlay', - strokeWidth: 1, - zIndex: 1, - }, - - '&[data-disabled]': { - opacity: 0.3, - }, - - '&:disabled': { - opacity: 0.3, - }, - - variants: { - bp: { - mobile: {}, - small: { - '& *[data-shy="true"]': { - opacity: 0, - }, - '&:hover:not(:disabled)': { - backgroundColor: '$hover', - '& *[data-shy="true"]': { - opacity: 1, - }, - }, - }, - }, - size: { - icon: { - padding: '4px ', - width: 'auto', - }, - }, - variant: { - noIcon: { - padding: '4px 12px', - }, - pageButton: { - display: 'grid', - gridTemplateColumns: '24px auto', - width: '100%', - paddingLeft: '$1', - gap: '$3', - justifyContent: 'flex-start', - [`& > *[data-state="checked"]`]: { - gridRow: 1, - gridColumn: 1, - }, - '& > span': { - gridRow: 1, - gridColumn: 2, - width: '100%', - }, - }, - }, - warn: { - true: { - color: '$warn', - }, - }, - isActive: { - true: { - backgroundColor: '$hover', - }, - }, - }, -}) diff --git a/packages/tldraw/src/components/style-panel/align-distribute.tsx b/packages/tldraw/src/components/style-panel/align-distribute.tsx deleted file mode 100644 index cffafeb84..000000000 --- a/packages/tldraw/src/components/style-panel/align-distribute.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import * as React from 'react' -import { - AlignBottomIcon, - AlignCenterHorizontallyIcon, - AlignCenterVerticallyIcon, - AlignLeftIcon, - AlignRightIcon, - AlignTopIcon, - SpaceEvenlyHorizontallyIcon, - SpaceEvenlyVerticallyIcon, - StretchHorizontallyIcon, - StretchVerticallyIcon, -} from '@radix-ui/react-icons' -import { AlignType, DistributeType, StretchType } from '~types' -import { useTLDrawContext } from '~hooks' -import { breakpoints, buttonsRow, iconButton } from '../shared' - -export interface AlignDistributeProps { - hasTwoOrMore: boolean - hasThreeOrMore: boolean -} - -export const AlignDistribute = React.memo( - ({ hasTwoOrMore, hasThreeOrMore }: AlignDistributeProps): JSX.Element => { - const { tlstate } = useTLDrawContext() - - const alignTop = React.useCallback(() => { - tlstate.align(AlignType.Top) - }, [tlstate]) - - const alignCenterVertical = React.useCallback(() => { - tlstate.align(AlignType.CenterVertical) - }, [tlstate]) - - const alignBottom = React.useCallback(() => { - tlstate.align(AlignType.Bottom) - }, [tlstate]) - - const stretchVertically = React.useCallback(() => { - tlstate.stretch(StretchType.Vertical) - }, [tlstate]) - - const distributeVertically = React.useCallback(() => { - tlstate.distribute(DistributeType.Vertical) - }, [tlstate]) - - const alignLeft = React.useCallback(() => { - tlstate.align(AlignType.Left) - }, [tlstate]) - - const alignCenterHorizontal = React.useCallback(() => { - tlstate.align(AlignType.CenterHorizontal) - }, [tlstate]) - - const alignRight = React.useCallback(() => { - tlstate.align(AlignType.Right) - }, [tlstate]) - - const stretchHorizontally = React.useCallback(() => { - tlstate.stretch(StretchType.Horizontal) - }, [tlstate]) - - const distributeHorizontally = React.useCallback(() => { - tlstate.distribute(DistributeType.Horizontal) - }, [tlstate]) - - return ( - <> -
- - - - - -
-
- - - - - -
- - ) - } -) diff --git a/packages/tldraw/src/components/style-panel/index.ts b/packages/tldraw/src/components/style-panel/index.ts deleted file mode 100644 index 01bd999c5..000000000 --- a/packages/tldraw/src/components/style-panel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './style-panel' diff --git a/packages/tldraw/src/components/style-panel/quick-color-select.tsx b/packages/tldraw/src/components/style-panel/quick-color-select.tsx deleted file mode 100644 index 6bd7c022b..000000000 --- a/packages/tldraw/src/components/style-panel/quick-color-select.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { BoxIcon, dropdownItem, dropdownContent } from './styled' -import { DropdownMenuIconTriggerButton } from '../shared' -import { strokes } from '~shape-utils' -import { useTheme, useTLDrawContext } from '~hooks' -import type { Data, ColorStyle } from '~types' - -const selectColor = (s: Data) => s.appState.selectedStyle.color - -export const QuickColorSelect = React.memo((): JSX.Element => { - const { theme } = useTheme() - const { tlstate, useSelector } = useTLDrawContext() - - const color = useSelector(selectColor) - - const handleColorChange = React.useCallback( - (color) => tlstate.style({ color: color as ColorStyle }), - [tlstate] - ) - - return ( - - - - - - - {Object.keys(strokes[theme]).map((colorStyle: string) => ( - - - - ))} - - - - ) -}) diff --git a/packages/tldraw/src/components/style-panel/quick-dash-select.tsx b/packages/tldraw/src/components/style-panel/quick-dash-select.tsx deleted file mode 100644 index 0b6df0efd..000000000 --- a/packages/tldraw/src/components/style-panel/quick-dash-select.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from 'react' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { DropdownMenuIconTriggerButton } from '../shared' -import { - DashDrawIcon, - DashDottedIcon, - DashSolidIcon, - DashDashedIcon, - dropdownContent, - dropdownItem, -} from './styled' -import { useTLDrawContext } from '~hooks' -import { DashStyle, Data } from '~types' - -const dashes = { - [DashStyle.Draw]: , - [DashStyle.Solid]: , - [DashStyle.Dashed]: , - [DashStyle.Dotted]: , -} - -const selectDash = (s: Data) => s.appState.selectedStyle.dash - -export const QuickDashSelect = React.memo((): JSX.Element => { - const { tlstate, useSelector } = useTLDrawContext() - - const dash = useSelector(selectDash) - - const changeDashStyle = React.useCallback( - (dash) => tlstate.style({ dash: dash as DashStyle }), - [tlstate] - ) - - return ( - - {dashes[dash]} - - - {Object.keys(DashStyle).map((dashStyle: string) => ( - - {dashes[dashStyle as DashStyle]} - - ))} - - - - ) -}) diff --git a/packages/tldraw/src/components/style-panel/quick-fill-select.tsx b/packages/tldraw/src/components/style-panel/quick-fill-select.tsx deleted file mode 100644 index 8bd895daa..000000000 --- a/packages/tldraw/src/components/style-panel/quick-fill-select.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from 'react' -import * as Checkbox from '@radix-ui/react-checkbox' -import { BoxIcon, IsFilledFillIcon } from './styled' -import { breakpoints, Tooltip, iconButton, iconWrapper } from '../shared' -import { useTLDrawContext } from '~hooks' -import type { Data } from '~types' - -const isFilledSelector = (s: Data) => s.appState.selectedStyle.isFilled - -export const QuickFillSelect = React.memo((): JSX.Element => { - const { tlstate, useSelector } = useTLDrawContext() - - const isFilled = useSelector(isFilledSelector) - - const handleIsFilledChange = React.useCallback( - (isFilled: boolean) => tlstate.style({ isFilled }), - [tlstate] - ) - - return ( - - -
- - - - -
-
-
- ) -}) diff --git a/packages/tldraw/src/components/style-panel/quick-size-select.tsx b/packages/tldraw/src/components/style-panel/quick-size-select.tsx deleted file mode 100644 index 1c81a03b4..000000000 --- a/packages/tldraw/src/components/style-panel/quick-size-select.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from 'react' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { DropdownMenuIconTriggerButton } from '../shared/dropdown-menu' -import { CircleIcon } from '../icons' -import { dropdownContent, dropdownItem } from './styled' -import { Data, SizeStyle } from '~types' -import { useTLDrawContext } from '~hooks' - -const sizes = { - [SizeStyle.Small]: 6, - [SizeStyle.Medium]: 12, - [SizeStyle.Large]: 22, -} - -const selectSize = (s: Data) => s.appState.selectedStyle.size - -export const QuickSizeSelect = React.memo((): JSX.Element => { - const { tlstate, useSelector } = useTLDrawContext() - - const size = useSelector(selectSize) - - const changeSizeStyle = React.useCallback( - (size: string) => tlstate.style({ size: size as SizeStyle }), - [tlstate] - ) - - return ( - - - - - - - {Object.keys(SizeStyle).map((sizeStyle: string) => ( - - - - ))} - - - - ) -}) diff --git a/packages/tldraw/src/components/style-panel/shapes-functions.tsx b/packages/tldraw/src/components/style-panel/shapes-functions.tsx deleted file mode 100644 index bb4c4aed4..000000000 --- a/packages/tldraw/src/components/style-panel/shapes-functions.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import * as React from 'react' -import { iconButton, buttonsRow, breakpoints } from '../shared' -import { Trash } from '../icons' -import { Tooltip } from '../shared/tooltip' -import { - ArrowDownIcon, - ArrowUpIcon, - AspectRatioIcon, - CopyIcon, - GroupIcon, - LockClosedIcon, - LockOpen1Icon, - PinBottomIcon, - PinTopIcon, - RotateCounterClockwiseIcon, -} from '@radix-ui/react-icons' -import { useTLDrawContext } from '~hooks' -import type { Data } from '~types' - -const isAllLockedSelector = (s: Data) => { - const page = s.document.pages[s.appState.currentPageId] - const { selectedIds } = s.document.pageStates[s.appState.currentPageId] - return selectedIds.every((id) => page.shapes[id].isLocked) -} - -const isAllAspectLockedSelector = (s: Data) => { - const page = s.document.pages[s.appState.currentPageId] - const { selectedIds } = s.document.pageStates[s.appState.currentPageId] - return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked) -} - -const isAllGroupedSelector = (s: Data) => { - const page = s.document.pages[s.appState.currentPageId] - const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map( - (id) => page.shapes[id] - ) - - return selectedShapes.every( - (shape) => - shape.children !== undefined || - (shape.parentId === selectedShapes[0].parentId && - selectedShapes[0].parentId !== s.appState.currentPageId) - ) -} - -const hasSelectionSelector = (s: Data) => { - const { selectedIds } = s.document.pageStates[s.appState.currentPageId] - return selectedIds.length > 0 -} - -const hasMultipleSelectionSelector = (s: Data) => { - const { selectedIds } = s.document.pageStates[s.appState.currentPageId] - return selectedIds.length > 1 -} - -export const ShapesFunctions = React.memo(() => { - const { tlstate, useSelector } = useTLDrawContext() - - const isAllLocked = useSelector(isAllLockedSelector) - - const isAllAspectLocked = useSelector(isAllAspectLockedSelector) - - const isAllGrouped = useSelector(isAllGroupedSelector) - - const hasSelection = useSelector(hasSelectionSelector) - - const hasMultipleSelection = useSelector(hasMultipleSelectionSelector) - - const handleRotate = React.useCallback(() => { - tlstate.rotate() - }, [tlstate]) - - const handleDuplicate = React.useCallback(() => { - tlstate.duplicate() - }, [tlstate]) - - const handleToggleLocked = React.useCallback(() => { - tlstate.toggleLocked() - }, [tlstate]) - - const handleToggleAspectRatio = React.useCallback(() => { - tlstate.toggleAspectRatioLocked() - }, [tlstate]) - - const handleGroup = React.useCallback(() => { - tlstate.group() - }, [tlstate]) - - const handleMoveToBack = React.useCallback(() => { - tlstate.moveToBack() - }, [tlstate]) - - const handleMoveBackward = React.useCallback(() => { - tlstate.moveBackward() - }, [tlstate]) - - const handleMoveForward = React.useCallback(() => { - tlstate.moveForward() - }, [tlstate]) - - const handleMoveToFront = React.useCallback(() => { - tlstate.moveToFront() - }, [tlstate]) - - const handleDelete = React.useCallback(() => { - tlstate.delete() - }, [tlstate]) - - return ( - <> -
- - - - - - - - - -
-
- - - - - - - - - -
- - ) -}) diff --git a/packages/tldraw/src/components/style-panel/style-panel.test.tsx b/packages/tldraw/src/components/style-panel/style-panel.test.tsx deleted file mode 100644 index 985c597b6..000000000 --- a/packages/tldraw/src/components/style-panel/style-panel.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from 'react' -import { renderWithContext } from '~test' -import { StylePanel } from './style-panel' - -describe('style panel', () => { - test('mounts component without crashing', () => { - renderWithContext() - }) -}) diff --git a/packages/tldraw/src/components/style-panel/style-panel.tsx b/packages/tldraw/src/components/style-panel/style-panel.tsx deleted file mode 100644 index 8dff7d72f..000000000 --- a/packages/tldraw/src/components/style-panel/style-panel.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import * as React from 'react' -import { Utils } from '@tldraw/core' -import { DotsHorizontalIcon, Cross2Icon } from '@radix-ui/react-icons' -import { useTLDrawContext } from '~hooks' -import type { Data } from '~types' -import { ShapesFunctions } from './shapes-functions' -import { AlignDistribute } from './align-distribute' -import { QuickColorSelect } from './quick-color-select' -import { QuickSizeSelect } from './quick-size-select' -import { QuickDashSelect } from './quick-dash-select' -import { QuickFillSelect } from './quick-fill-select' -import { Tooltip } from '../shared/tooltip' -import { - Kbd, - iconButton, - buttonsRow, - breakpoints, - rowButton, - floatingContainer, - divider, -} from '../shared' - -const isStyleOpenSelector = (s: Data) => s.appState.isStyleOpen - -export function StylePanel(): JSX.Element { - const { tlstate, useSelector } = useTLDrawContext() - const isOpen = useSelector(isStyleOpenSelector) - - return ( -
-
- - - - - -
- {isOpen && } -
- ) -} - -const selectedShapesCountSelector = (s: Data) => - s.document.pageStates[s.appState.currentPageId].selectedIds.length - -function SelectedShapeContent(): JSX.Element { - const { tlstate, useSelector } = useTLDrawContext() - const selectedShapesCount = useSelector(selectedShapesCountSelector) - - const [showKbds] = React.useState(() => !Utils.isMobileSize()) - - const handleCopy = React.useCallback(() => { - tlstate.copy() - }, [tlstate]) - - const handlePaste = React.useCallback(() => { - tlstate.paste() - }, [tlstate]) - - const handleCopySvg = React.useCallback(() => { - tlstate.copySvg() - }, [tlstate]) - - return ( - <> -
- -
- 1} - hasThreeOrMore={selectedShapesCount > 2} - /> -
- - - - - ) -} diff --git a/packages/tldraw/src/components/style-panel/styled.tsx b/packages/tldraw/src/components/style-panel/styled.tsx deleted file mode 100644 index 9dda9f363..000000000 --- a/packages/tldraw/src/components/style-panel/styled.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import * as React from 'react' -import css from '~styles' - -export const dropdownContent = css({ - display: 'grid', - padding: 4, - gridTemplateColumns: 'repeat(4, 1fr)', - backgroundColor: '$panel', - borderRadius: 4, - border: '1px solid $panel', - boxShadow: '$4', - - variants: { - direction: { - vertical: { - gridTemplateColumns: '1fr', - }, - }, - }, -}) - -export const dropdownItem = css({ - height: '32px', - width: '32px', - backgroundColor: '$panel', - borderRadius: '4px', - padding: '0', - margin: '0', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - outline: 'none', - border: 'none', - pointerEvents: 'all', - cursor: 'pointer', - - '&:focus': { - backgroundColor: '$hover', - }, - - '&:hover:not(:disabled)': { - backgroundColor: '$hover', - }, - - '&:disabled': { - opacity: '0.5', - }, - - variants: { - isActive: { - true: { - '& svg': { - fill: '$text', - stroke: '$text', - }, - }, - false: { - '& svg': { - fill: '$inactive', - stroke: '$inactive', - }, - }, - }, - }, -}) - -export function BoxIcon({ - fill = 'none', - stroke = 'currentColor', -}: { - fill?: string - stroke?: string -}): JSX.Element { - return ( - - - - ) -} - -export function DashSolidIcon(): JSX.Element { - return ( - - - - ) -} - -export function DashDashedIcon(): JSX.Element { - return ( - - - - ) -} - -const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}` - -export function DashDottedIcon(): JSX.Element { - return ( - - - - ) -} - -export function DashDrawIcon(): JSX.Element { - return ( - - - - ) -} - -export function IsFilledFillIcon(): JSX.Element { - return ( - - - - ) -} diff --git a/packages/tldraw/src/components/tldraw/index.ts b/packages/tldraw/src/components/tldraw/index.ts deleted file mode 100644 index b7df7d774..000000000 --- a/packages/tldraw/src/components/tldraw/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tldraw' diff --git a/packages/tldraw/src/components/tools-panel/back-to-content/index.ts b/packages/tldraw/src/components/tools-panel/back-to-content/index.ts deleted file mode 100644 index bd1cb3db6..000000000 --- a/packages/tldraw/src/components/tools-panel/back-to-content/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './back-to-content' diff --git a/packages/tldraw/src/components/tools-panel/index.ts b/packages/tldraw/src/components/tools-panel/index.ts deleted file mode 100644 index a2728adb6..000000000 --- a/packages/tldraw/src/components/tools-panel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tools-panel' diff --git a/packages/tldraw/src/components/tools-panel/primary-tools/index.ts b/packages/tldraw/src/components/tools-panel/primary-tools/index.ts deleted file mode 100644 index 3aea0d2b0..000000000 --- a/packages/tldraw/src/components/tools-panel/primary-tools/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './primary-tools' diff --git a/packages/tldraw/src/components/tools-panel/status-bar/index.ts b/packages/tldraw/src/components/tools-panel/status-bar/index.ts deleted file mode 100644 index f9d6aa7b1..000000000 --- a/packages/tldraw/src/components/tools-panel/status-bar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './status-bar' diff --git a/packages/tldraw/src/components/tools-panel/styled.tsx b/packages/tldraw/src/components/tools-panel/styled.tsx deleted file mode 100644 index 4910aebf3..000000000 --- a/packages/tldraw/src/components/tools-panel/styled.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import * as React from 'react' -import { floatingContainer } from '../shared' -import { Tooltip } from '../shared/tooltip' -import css from '~styles' - -export const toolButton = css({ - position: 'relative', - height: '32px', - width: '32px', - color: '$text', - backgroundColor: '$panel', - borderRadius: '4px', - padding: '0', - margin: '0', - display: 'grid', - alignItems: 'center', - justifyContent: 'center', - outline: 'none', - border: 'none', - pointerEvents: 'all', - fontSize: '$0', - cursor: 'pointer', - - '& > *': { - gridRow: 1, - gridColumn: 1, - }, - - '&:disabled': { - opacity: '0.5', - }, - - '& > span': { - width: '100%', - height: '100%', - display: 'flex', - alignItems: 'center', - }, -}) - -export const primaryToolButton = css(toolButton, { - variants: { - bp: { - mobile: { - height: 44, - width: 36, - '& svg:nth-of-type(1)': { - height: '20px', - width: '20px', - }, - }, - small: { - width: 44, - '&:hover:not(:disabled)': { - backgroundColor: '$hover', - }, - }, - medium: {}, - large: {}, - }, - isActive: { - true: { - color: '$selected', - }, - }, - }, -}) - -export const secondaryToolButton = css(toolButton, { - variants: { - bp: { - mobile: { - height: 44, - width: 36, - '& svg:nth-of-type(1)': { - height: '18px', - width: '18px', - }, - }, - small: { - width: 44, - '&:hover:not(:disabled)': { - backgroundColor: '$hover', - }, - }, - medium: {}, - large: {}, - }, - isActive: { - true: { - color: '$selected', - }, - }, - }, -}) - -export const tertiaryToolButton = css(toolButton, { - variants: { - bp: { - mobile: { - height: 32, - width: 36, - '& svg:nth-of-type(1)': { - height: '16px', - width: '16px', - }, - }, - small: { - height: 40, - width: 40, - '& svg:nth-of-type(1)': { - height: '18px', - width: '18px', - }, - '&:hover:not(:disabled)': { - backgroundColor: '$hover', - }, - }, - medium: {}, - large: {}, - }, - }, -}) - -interface PrimaryToolButtonProps { - label: string - kbd: string - onClick: () => void - onDoubleClick?: () => void - isActive: boolean - children: React.ReactNode -} - -export function PrimaryButton({ - label, - kbd, - onClick, - onDoubleClick, - isActive, - children, -}: PrimaryToolButtonProps): JSX.Element { - return ( - - - - ) -} - -interface SecondaryToolButtonProps { - label: string - kbd: string - onClick: () => void - onDoubleClick?: () => void - isActive: boolean - children: React.ReactNode -} - -export function SecondaryButton({ - label, - kbd, - onClick, - onDoubleClick, - isActive, - children, -}: SecondaryToolButtonProps): JSX.Element { - return ( - - - - ) -} - -interface TertiaryToolProps { - label: string - kbd: string - onClick: () => void - onDoubleClick?: () => void - children: React.ReactNode -} - -export function TertiaryButton({ - label, - kbd, - onClick, - onDoubleClick, - children, -}: TertiaryToolProps): JSX.Element { - return ( - - - - ) -} - -export const tertiaryButtonsContainer = css(floatingContainer, { - boxShadow: '$3', - variants: { - bp: { - mobile: { - alignItems: 'center', - flexDirection: 'column', - }, - small: { - alignItems: 'center', - flexDirection: 'row', - }, - }, - }, -}) diff --git a/packages/tldraw/src/components/tools-panel/tools-panel.tsx b/packages/tldraw/src/components/tools-panel/tools-panel.tsx deleted file mode 100644 index a4ac0ab84..000000000 --- a/packages/tldraw/src/components/tools-panel/tools-panel.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import * as React from 'react' -import { CursorArrowIcon, LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons' -import css from '~styles' -import type { Data } from '~types' -import { useTLDrawContext } from '~hooks' -import { floatingContainer } from '~components/shared' -import { StatusBar } from '~components/tools-panel/status-bar' -import { SecondaryButton } from '~components/tools-panel/styled' -import { UndoRedo } from '~components/tools-panel/undo-redo' -import { Zoom } from '~components/tools-panel/zoom' -import { BackToContent } from '~components/tools-panel/back-to-content' -import { PrimaryTools } from '~components/tools-panel/primary-tools' - -const activeToolSelector = (s: Data) => s.appState.activeTool -const isToolLockedSelector = (s: Data) => s.appState.isToolLocked -const isDebugModeSelector = (s: Data) => s.settings.isDebugMode - -export const ToolsPanel = React.memo((): JSX.Element => { - const { tlstate, useSelector } = useTLDrawContext() - - const activeTool = useSelector(activeToolSelector) - - const isToolLocked = useSelector(isToolLockedSelector) - - const isDebugMode = useSelector(isDebugModeSelector) - - const selectSelectTool = React.useCallback(() => { - tlstate.selectTool('select') - }, [tlstate]) - - return ( -
-
- -
- - - -
-
-
- - -
-
-
- - {isToolLocked ? : } - -
- -
- {isDebugMode && ( -
- -
- )} -
- ) -}) - -const toolsPanelContainer = css({ - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - width: '100%', - minWidth: 0, - maxWidth: '100%', - display: 'grid', - gridTemplateColumns: '1fr auto 1fr', - gridTemplateRows: 'auto auto', - padding: '0', - alignItems: 'flex-end', - zIndex: 200, - gridGap: '$4', - gridRowGap: '$4', - pointerEvents: 'none', - '& > div > *': { - pointerEvents: 'all', - }, -}) - -const centerWrap = css({ - gridRow: 1, - gridColumn: 2, - display: 'flex', - width: 'fit-content', - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'column', - gap: 12, -}) - -const leftWrap = css({ - gridRow: 1, - gridColumn: 1, - display: 'flex', - paddingLeft: '$3', - variants: { - size: { - mobile: { - flexDirection: 'column', - justifyContent: 'flex-end', - alignItems: 'flex-start', - '& > *:nth-of-type(1)': { - marginBottom: '8px', - }, - }, - small: { - flexDirection: 'row', - alignItems: 'flex-end', - justifyContent: 'space-between', - '& > *:nth-of-type(1)': { - marginBottom: '0px', - }, - }, - }, - }, -}) - -const rightWrap = css({ - gridRow: 1, - gridColumn: 3, - display: 'flex', - paddingRight: '$3', - opacity: 1, - variants: { - size: { - micro: { - opacity: 0, - }, - mobile: { - flexDirection: 'column-reverse', - justifyContent: 'flex-end', - alignItems: 'flex-end', - '& > *:nth-of-type(2)': { - marginBottom: '8px', - }, - opacity: 1, - }, - small: { - flexDirection: 'row', - alignItems: 'flex-end', - justifyContent: 'space-between', - '& > *:nth-of-type(2)': { - marginBottom: '0px', - }, - opacity: 1, - }, - }, - }, -}) - -const statusWrap = css({ - gridRow: 2, - gridColumn: '1 / span 3', -}) diff --git a/packages/tldraw/src/components/tools-panel/undo-redo/index.ts b/packages/tldraw/src/components/tools-panel/undo-redo/index.ts deleted file mode 100644 index 5e0211785..000000000 --- a/packages/tldraw/src/components/tools-panel/undo-redo/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './undo-redo' diff --git a/packages/tldraw/src/components/tools-panel/undo-redo/undo-redo.tsx b/packages/tldraw/src/components/tools-panel/undo-redo/undo-redo.tsx deleted file mode 100644 index 5564c41b0..000000000 --- a/packages/tldraw/src/components/tools-panel/undo-redo/undo-redo.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react' -import { useTLDrawContext } from '~hooks' -import { TertiaryButton, tertiaryButtonsContainer } from '~components/tools-panel/styled' -import { Undo, Redo, Trash } from '~components/icons' - -export const UndoRedo = React.memo((): JSX.Element => { - const { tlstate } = useTLDrawContext() - - const handleDelete = React.useCallback(() => { - tlstate.delete() - }, [tlstate]) - - const handleClear = React.useCallback(() => { - tlstate.clear() - }, [tlstate]) - - return ( -
- - - - - - - - - -
- ) -}) diff --git a/packages/tldraw/src/components/tools-panel/zoom/index.ts b/packages/tldraw/src/components/tools-panel/zoom/index.ts deleted file mode 100644 index 7d9ae51b7..000000000 --- a/packages/tldraw/src/components/tools-panel/zoom/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './zoom' diff --git a/packages/tldraw/src/components/tools-panel/zoom/zoom.tsx b/packages/tldraw/src/components/tools-panel/zoom/zoom.tsx deleted file mode 100644 index 656edf3fb..000000000 --- a/packages/tldraw/src/components/tools-panel/zoom/zoom.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react' -import { ZoomInIcon, ZoomOutIcon } from '@radix-ui/react-icons' -import { TertiaryButton, tertiaryButtonsContainer } from '~components/tools-panel/styled' -import { useTLDrawContext } from '~hooks' -import type { Data } from '~types' - -export const Zoom = React.memo((): JSX.Element => { - const { tlstate } = useTLDrawContext() - - return ( -
- - - - - - - -
- ) -}) - -const zoomSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId].camera.zoom - -function ZoomCounter() { - const { tlstate, useSelector } = useTLDrawContext() - const zoom = useSelector(zoomSelector) - - return ( - - {Math.round(zoom * 100)}% - - ) -} diff --git a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx index 481ee54c5..59d354070 100644 --- a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx +++ b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx @@ -261,7 +261,9 @@ export function useKeyboardShortcuts(ref: React.RefObject) { useHotkeys( 'escape', () => { - if (canHandleEvent()) tlstate.cancel() + if (canHandleEvent()) { + tlstate.cancel() + } }, undefined, [tlstate] diff --git a/packages/tldraw/src/index.ts b/packages/tldraw/src/index.ts index 5b8376434..2e45b1fdf 100644 --- a/packages/tldraw/src/index.ts +++ b/packages/tldraw/src/index.ts @@ -1,4 +1,4 @@ -export * from './components/tldraw' +export * from './TLDraw' export * from './types' export * from './shape-utils' export { TLDrawState } from './state' diff --git a/packages/tldraw/src/shape-utils/group/group.tsx b/packages/tldraw/src/shape-utils/group/group.tsx index 6dbefd41a..c3d109ea6 100644 --- a/packages/tldraw/src/shape-utils/group/group.tsx +++ b/packages/tldraw/src/shape-utils/group/group.tsx @@ -5,7 +5,7 @@ import { TLDrawShapeType, GroupShape, ColorStyle, TLDrawMeta } from '~types' import { getBoundsRectangle } from '../shared' import { BINDING_DISTANCE } from '~constants' import { TLDrawShapeUtil } from '../TLDrawShapeUtil' -import css from '~styles' +import styled from '~styles' type T = GroupShape type E = SVGSVGElement @@ -71,15 +71,14 @@ export class GroupUtil extends TLDrawShapeUtil { fill="transparent" pointerEvents="all" /> - {paths} - + ) } @@ -104,9 +103,9 @@ export class GroupUtil extends TLDrawShapeUtil { }) return ( - + {paths} - + ) }) @@ -119,7 +118,7 @@ export class GroupUtil extends TLDrawShapeUtil { } } -const scaledLines = css({ +const ScaledLines = styled('g', { strokeWidth: 'calc(1.5px * var(--tl-scale))', strokeDasharray: `calc(1px * var(--tl-scale)), calc(3px * var(--tl-scale))`, }) diff --git a/packages/tldraw/src/shape-utils/sticky/sticky.tsx b/packages/tldraw/src/shape-utils/sticky/sticky.tsx index e68b2e084..3eb3f9cdf 100644 --- a/packages/tldraw/src/shape-utils/sticky/sticky.tsx +++ b/packages/tldraw/src/shape-utils/sticky/sticky.tsx @@ -6,7 +6,7 @@ import { StickyShape, TLDrawMeta, TLDrawShapeType, TLDrawTransformInfo } from '~ import { getBoundsRectangle, TextAreaUtils } from '../shared' import { TLDrawShapeUtil } from '../TLDrawShapeUtil' import { getStickyFontStyle, getStickyShapeStyle } from '../shape-styles' -import css from '~styles' +import styled from '~styles' import Vec from '@tldraw/vec' type T = StickyShape @@ -163,18 +163,17 @@ export class StickyUtil extends TLDrawShapeUtil { return ( -
-
+ {shape.text}​ -
+ {isEditing && ( -