diff --git a/resources/css/common.css b/resources/css/common.css index 09f8f930dd..aafc79a41f 100644 --- a/resources/css/common.css +++ b/resources/css/common.css @@ -931,6 +931,8 @@ button.menu:focus { @apply my-1; opacity: .5; + border-top-width: 1px; + border-color: var(--ls-border-color, #ccc); } a.login { diff --git a/tldraw/.editorconfig b/tldraw/.editorconfig new file mode 100644 index 0000000000..aabed14a9c --- /dev/null +++ b/tldraw/.editorconfig @@ -0,0 +1,19 @@ + +# https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 80 +trim_trailing_whitespace = true + +[*.md] +max_line_length = 0 +trim_trailing_whitespace = false + +[COMMIT_EDITMSG] +max_line_length = 0 diff --git a/tldraw/apps/tldraw-logseq/package.json b/tldraw/apps/tldraw-logseq/package.json index c29c549973..58f481b96f 100644 --- a/tldraw/apps/tldraw-logseq/package.json +++ b/tldraw/apps/tldraw-logseq/package.json @@ -10,6 +10,7 @@ "dev:vite": "tsup --watch --sourcemap inline" }, "devDependencies": { + "@radix-ui/react-context-menu": "^1.0.0", "@radix-ui/react-dropdown-menu": "^1.0.0", "@radix-ui/react-select": "^1.0.0", "@radix-ui/react-separator": "^1.0.0", diff --git a/tldraw/apps/tldraw-logseq/src/app.tsx b/tldraw/apps/tldraw-logseq/src/app.tsx index a96fb9a3a6..2815615ee8 100644 --- a/tldraw/apps/tldraw-logseq/src/app.tsx +++ b/tldraw/apps/tldraw-logseq/src/app.tsx @@ -11,6 +11,7 @@ import { import * as React from 'react' import { AppUI } from './components/AppUI' import { ContextBar } from './components/ContextBar' +import { ContextMenu } from './components/ContextMenu' import { useFileDrop } from './hooks/useFileDrop' import { usePaste } from './hooks/usePaste' import { useQuickAdd } from './hooks/useQuickAdd' @@ -80,6 +81,7 @@ export const App = function App({ const onFileDrop = useFileDrop(contextValue) const onPaste = usePaste(contextValue) const onQuickAdd = useQuickAdd() + const ref = React.useRef(null); const onPersistOnDiff: TLReactCallbacks['onPersist'] = React.useCallback( (app, info) => { @@ -102,11 +104,13 @@ export const App = function App({ model={model} {...rest} > -
- - - -
+ +
+ + + +
+
) diff --git a/tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx b/tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx new file mode 100644 index 0000000000..0a2a3d7f8f --- /dev/null +++ b/tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx @@ -0,0 +1,129 @@ +import { useApp } from '@tldraw/react' +import { MOD_KEY} from '@tldraw/core' +import { observer } from 'mobx-react-lite' +import * as React from 'react' + +import * as ReactContextMenu from '@radix-ui/react-context-menu' + +const preventDefault = (e: Event) => e.stopPropagation() + +interface ContextMenuProps { + children: React.ReactNode + collisionRef: React.RefObject +} + +export const ContextMenu = observer(function ContextMenu({ children, collisionRef }: ContextMenuProps) { + const app = useApp() + const rContent = React.useRef(null) + + return ( + {if (!state) app.transition('select')}}> + {children} + +
+ {app.selectedShapes?.size > 0 && ( + <> + app.copy()}> + Copy +
+ {MOD_KEY} C +
+
+ + )} + app.paste()}> + Paste +
+ {MOD_KEY} V +
+
+ + app.api.selectAll()}> + Select All +
+ {MOD_KEY} A +
+
+ {app.selectedShapes?.size > 1 && ( + app.api.deselectAll()}> + Deselect All + + )} + {app.selectedShapes?.size > 0 && ( + <> + app.api.deleteShapes()}> + Delete +
+ Del +
+
+ {app.selectedShapes?.size > 1 && ( + <> + + app.flipHorizontal()}> + Flip Horizontally + + app.flipVertical()}> + Flip Vertically + + + )} + + app.bringToFront()}> + Move to Front +
+ ] +
+
+ + Move forwards +
+ ] +
+
+ app.sendToBack()}> + Move to back +
+ [ +
+
+ app.sendBackward()}> + Move backwards +
+ [ +
+
+ + )} +
+
+
+ ) +}) diff --git a/tldraw/apps/tldraw-logseq/src/components/ContextMenu/index.ts b/tldraw/apps/tldraw-logseq/src/components/ContextMenu/index.ts new file mode 100644 index 0000000000..47f4873934 --- /dev/null +++ b/tldraw/apps/tldraw-logseq/src/components/ContextMenu/index.ts @@ -0,0 +1 @@ +export * from './ContextMenu' diff --git a/tldraw/apps/tldraw-logseq/src/components/ZoomMenu/ZoomMenu.tsx b/tldraw/apps/tldraw-logseq/src/components/ZoomMenu/ZoomMenu.tsx index b7ced7731c..17c4307ee5 100644 --- a/tldraw/apps/tldraw-logseq/src/components/ZoomMenu/ZoomMenu.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/ZoomMenu/ZoomMenu.tsx @@ -1,5 +1,6 @@ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' import { useApp } from '@tldraw/react' +import { MOD_KEY} from '@tldraw/core' import { observer } from 'mobx-react-lite' export const ZoomMenu = observer(function ZoomMenu(): JSX.Element { @@ -19,39 +20,39 @@ export const ZoomMenu = observer(function ZoomMenu(): JSX.Element { > Zoom to Fit
- Zoom to Selection
⌘+Minus
+ Zoom to Selection
{MOD_KEY} -
- Zoom In
⌘+Plus
+ Zoom In
{MOD_KEY} +
- Zoom Out
⌘+Minus
+ Zoom Out
{MOD_KEY} -
- Reset Zoom
⇧+0
+ Reset Zoom
0
diff --git a/tldraw/apps/tldraw-logseq/src/styles.css b/tldraw/apps/tldraw-logseq/src/styles.css index 589191c158..78a428723c 100644 --- a/tldraw/apps/tldraw-logseq/src/styles.css +++ b/tldraw/apps/tldraw-logseq/src/styles.css @@ -36,6 +36,45 @@ font-size: inherit; } +.tl-context-menu-button { + @apply flex items-center px-4 py-1 text-sm !important; + + min-width: 220px; + all: unset; + line-height: 1; + height: 25px; + padding: 0 5px; + position: relative; + user-select: none; + color: var(--ls-primary-text-color); + + &:hover, + &:focus { + background-color: var(--ls-secondary-background-color) !important; + } +} + +.tl-context-menu { + @apply relative py-2 flex bottom-0 flex border-0 rounded shadow-lg; + + opacity: 100%; + user-select: none; + flex-direction: column; + z-index: 180; + min-width: 220px; + pointer-events: 'all'; + background: var(--ls-primary-background-color); +} + +.tl-context-menu-right-slot { + margin-left: auto; + padding-left: 20px; +} + +.tl-context-menu-right-slot:focus { + color: whites; +} + .tl-action-bar { @apply absolute bottom-0 flex border-0; @@ -69,33 +108,26 @@ } .tl-zoom-menu-dropdown-menu-button { + @apply py-2 rounded shadow-lg; opacity: 100%; - background-color: var(--color-panel); - border-radius: 6px; - padding: 5px; - box-shadow: 0 10px 38px -10px rgba(22, 23, 24, 0.35), 0 10px 20px -15px rgba(22, 23, 24, 0.2); + background-color: var(--ls-primary-background-color); + > span svg { - fill: var(--color-panel) !important; + display: none !important; } } .tl-zoom-menu-dropdown-item { + @apply flex items-center px-4 py-1 text-sm !important; min-width: 220px; all: unset; - font-size: 13px; - line-height: 1; - color: black; - border-radius: 3px; - display: flex; - align-items: center; height: 25px; - padding: 0 5px; - position: relative; user-select: none; color: var(--color-text); &:hover { + cursor: pointer; background-color: var(--color-hover); } } @@ -103,7 +135,6 @@ .tl-zoom-menu-right-slot { margin-left: auto; padding-left: 20px; - color: var(--color-text); } .tl-contextbar { diff --git a/tldraw/packages/core/src/constants.ts b/tldraw/packages/core/src/constants.ts index 39f2715c96..40bc86451b 100644 --- a/tldraw/packages/core/src/constants.ts +++ b/tldraw/packages/core/src/constants.ts @@ -22,6 +22,8 @@ export const FIT_TO_SCREEN_PADDING = 100 export const BINDING_DISTANCE = 4 +export const ZOOM_UPDATE_FACTOR = 0.8 + export const GRID_SIZE = 8 export const EMPTY_OBJECT: any = {} diff --git a/tldraw/packages/core/src/lib/TLApp/TLApp.ts b/tldraw/packages/core/src/lib/TLApp/TLApp.ts index 673a291977..9d8adaae75 100644 --- a/tldraw/packages/core/src/lib/TLApp/TLApp.ts +++ b/tldraw/packages/core/src/lib/TLApp/TLApp.ts @@ -806,16 +806,42 @@ export class TLApp< /* ----------------- Event Handlers ----------------- */ + temporaryTransitionToMove(event: any) { + event.stopPropagation() + event.preventDefault() + const prevTool = this.selectedTool + this.transition('move', { prevTool }) + this.selectedTool.transition('idleHold') + } + readonly onTransition: TLStateEvents['onTransition'] = () => { this.settings.update({ isToolLocked: false }) } readonly onWheel: TLEvents['wheel'] = (info, e) => { + if (e.ctrlKey) { + return + } + this.viewport.panCamera(info.delta) this.inputs.onWheel([...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], e) } readonly onPointerDown: TLEvents['pointer'] = (info, e) => { + + + // Pan canvas when holding middle click + if (!this.editingShape && e.button === 1 && !this.isIn('move')) { + this.temporaryTransitionToMove(e) + return + } + + // Switch to select on right click to enable contextMenu state + if (e.button === 2) { + this.transition('select') + return + } + if ('clientX' in e) { this.inputs.onPointerDown( [...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], @@ -825,6 +851,13 @@ export class TLApp< } readonly onPointerUp: TLEvents['pointer'] = (info, e) => { + if (!this.editingShape && e.button === 1 && this.isIn('move')) { + this.selectedTool.transition('idle', { exit: true }) + e.stopPropagation() + e.preventDefault() + return + } + if ('clientX' in e) { this.inputs.onPointerUp( [...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], @@ -841,11 +874,7 @@ export class TLApp< readonly onKeyDown: TLEvents['keyboard'] = (info, e) => { if (!this.editingShape && e['key'] === ' ' && !this.isIn('move')) { - e.stopPropagation() - e.preventDefault() - const prevTool = this.selectedTool - this.transition('move', { prevTool }) - this.selectedTool.transition('idleHold') + this.temporaryTransitionToMove(e) return } this.inputs.onKeyDown(e) diff --git a/tldraw/packages/core/src/lib/TLViewport.ts b/tldraw/packages/core/src/lib/TLViewport.ts index c97091cf84..628d8994d6 100644 --- a/tldraw/packages/core/src/lib/TLViewport.ts +++ b/tldraw/packages/core/src/lib/TLViewport.ts @@ -1,6 +1,6 @@ import { Vec } from '@tldraw/vec' import { action, computed, makeObservable, observable } from 'mobx' -import { FIT_TO_SCREEN_PADDING } from '../constants' +import { FIT_TO_SCREEN_PADDING, ZOOM_UPDATE_FACTOR } from '../constants' import type { TLBounds } from '../types' export class TLViewport { @@ -84,7 +84,7 @@ export class TLViewport { zoomIn = (): this => { const { camera, bounds } = this - const zoom: number = Math.min(TLViewport.maxZoom, Math.ceil((camera.zoom * 100 + 1) / 25) / 4) + const zoom: number = Math.min(TLViewport.maxZoom, camera.zoom / ZOOM_UPDATE_FACTOR) const center = [bounds.width / 2, bounds.height / 2] const p0 = Vec.sub(Vec.div(center, camera.zoom), center) const p1 = Vec.sub(Vec.div(center, zoom), center) @@ -93,7 +93,7 @@ export class TLViewport { zoomOut = (): this => { const { camera, bounds } = this - const zoom: number = Math.max(TLViewport.minZoom, Math.floor((camera.zoom * 100 - 1) / 25) / 4) + const zoom: number = Math.max(TLViewport.minZoom, camera.zoom * ZOOM_UPDATE_FACTOR) const center = [bounds.width / 2, bounds.height / 2] const p0 = Vec.sub(Vec.div(center, camera.zoom), center) const p1 = Vec.sub(Vec.div(center, zoom), center) diff --git a/tldraw/packages/core/src/lib/tools/TLSelectTool/TLSelectTool.tsx b/tldraw/packages/core/src/lib/tools/TLSelectTool/TLSelectTool.tsx index 4a939b73c6..b0ea6af7dc 100644 --- a/tldraw/packages/core/src/lib/tools/TLSelectTool/TLSelectTool.tsx +++ b/tldraw/packages/core/src/lib/tools/TLSelectTool/TLSelectTool.tsx @@ -5,6 +5,7 @@ import { TLTool } from '../../TLTool' import { IdleState, BrushingState, + ContextMenuState, PointingCanvasState, PointingShapeState, PointingShapeBehindBoundsState, @@ -37,6 +38,7 @@ export class TLSelectTool< static states = [ IdleState, BrushingState, + ContextMenuState, PointingCanvasState, PointingShapeState, PointingShapeBehindBoundsState, diff --git a/tldraw/packages/core/src/lib/tools/TLSelectTool/states/ContextMenuState.ts b/tldraw/packages/core/src/lib/tools/TLSelectTool/states/ContextMenuState.ts new file mode 100644 index 0000000000..a6a2c5f505 --- /dev/null +++ b/tldraw/packages/core/src/lib/tools/TLSelectTool/states/ContextMenuState.ts @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { TLShape } from '../../../shapes' +import type { TLApp } from '../../../TLApp' +import { TLToolState } from '../../../TLToolState' +import type { TLSelectTool } from '../TLSelectTool' +import type { TLEvents, TLSelectionHandle, TLEventMap, TLEventSelectionInfo } from '../../../../types' + +export class ContextMenuState< + S extends TLShape, + K extends TLEventMap, + R extends TLApp, + P extends TLSelectTool +> extends TLToolState { + static id = 'contextMenu' + + handle?: TLSelectionHandle + + onEnter = (info: TLEventSelectionInfo) => { + this.handle = info.handle + } + + onPointerDown: TLEvents['pointer'] = () => { + this.tool.transition('idle') + } +} diff --git a/tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts b/tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts index dfb0c9e7c6..08729e7685 100644 --- a/tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts +++ b/tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts @@ -46,6 +46,11 @@ export class IdleState< inputs: { ctrlKey }, } = this.app + if (event.button === 2) { + this.tool.transition('contextMenu') + return + } + // Holding ctrlKey should ignore shapes if (ctrlKey) { this.tool.transition('pointingCanvas') diff --git a/tldraw/packages/core/src/lib/tools/TLSelectTool/states/index.ts b/tldraw/packages/core/src/lib/tools/TLSelectTool/states/index.ts index 55b05175c5..94911f6947 100644 --- a/tldraw/packages/core/src/lib/tools/TLSelectTool/states/index.ts +++ b/tldraw/packages/core/src/lib/tools/TLSelectTool/states/index.ts @@ -1,4 +1,5 @@ export * from './BrushingState' +export * from './ContextMenuState' export * from './IdleState' export * from './PointingShapeState' export * from './PointingBoundsBackgroundState' diff --git a/tldraw/packages/core/src/utils/index.ts b/tldraw/packages/core/src/utils/index.ts index 4b93143ae9..aee41c9b51 100644 --- a/tldraw/packages/core/src/utils/index.ts +++ b/tldraw/packages/core/src/utils/index.ts @@ -77,6 +77,8 @@ export function modKey(e: any): boolean { return isDarwin() ? e.metaKey : e.ctrlKey } +export const MOD_KEY = isDarwin() ? '⌘' : 'Ctrl'; + export function isNonNullable(value: TValue): value is NonNullable { return Boolean(value) } diff --git a/tldraw/packages/react/src/components/ContextBarContainer/ContextBarContainer.tsx b/tldraw/packages/react/src/components/ContextBarContainer/ContextBarContainer.tsx index d577c24ec9..26b9d1eab2 100644 --- a/tldraw/packages/react/src/components/ContextBarContainer/ContextBarContainer.tsx +++ b/tldraw/packages/react/src/components/ContextBarContainer/ContextBarContainer.tsx @@ -56,22 +56,11 @@ export const ContextBarContainer = observer(function ContextBarContainer { - const elm = rBounds.current - if (!elm) return - if (hidden || !inView) { - elm.classList.add('tl-fade-out') - elm.classList.remove('tl-fade-in') - } else { - elm.classList.add('tl-fade-in') - elm.classList.remove('tl-fade-out') - } - }, [hidden, inView]) return (
void null const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) diff --git a/tldraw/yarn.lock b/tldraw/yarn.lock index fc420058ad..fa26657426 100644 --- a/tldraw/yarn.lock +++ b/tldraw/yarn.lock @@ -1512,6 +1512,19 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-context-menu@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-1.0.0.tgz#49f2ac5faafc8124add8069a39eab603b2437ba8" + integrity sha512-JkwOgdXwErwEEpsmgu0Ob8zD3gzWS1brPXnNGPyZEtR6/EYyDgruQYKiihXVsCrPCdrNUHawop9I1+6JTdXPTA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.0" + "@radix-ui/react-context" "1.0.0" + "@radix-ui/react-menu" "1.0.0" + "@radix-ui/react-primitive" "1.0.0" + "@radix-ui/react-use-callback-ref" "1.0.0" + "@radix-ui/react-use-controllable-state" "1.0.0" + "@radix-ui/react-context@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"