Explorar el Código

feat: collapse logseq shapes

Peng Xiao hace 3 años
padre
commit
e4c461833a

+ 2 - 1
tldraw/apps/tldraw-logseq/package.json

@@ -32,7 +32,8 @@
     "shadow-cljs": "^2.19.3",
     "tsup": "^6.1.2",
     "typescript": "^4.7.3",
-    "zx": "^6.2.4"
+    "zx": "^6.2.4",
+    "@radix-ui/react-switch": "^0.1.6-rc.40"
   },
   "peerDependencies": {
     "react": "^16.8.0 || ^17.0.0 || ^18.0.0",

+ 3 - 1
tldraw/apps/tldraw-logseq/src/components/inputs/ColorInput.tsx

@@ -8,7 +8,9 @@ export function ColorInput({ label, ...rest }: ColorInputProps) {
   return (
     <div className="input">
       <label htmlFor={`color-${label}`}>{label}</label>
-      <input className="color-input" name={`color-${label}`} type="color" {...rest} />
+      <div className="color-input-wrapper">
+        <input className="color-input" name={`color-${label}`} type="color" {...rest} />
+      </div>
     </div>
   )
 }

+ 16 - 0
tldraw/apps/tldraw-logseq/src/components/inputs/SelectInput.tsx

@@ -0,0 +1,16 @@
+import * as React from 'react'
+
+interface ColorInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
+  label: string
+}
+
+export function ColorInput({ label, ...rest }: ColorInputProps) {
+  return (
+    <div className="input">
+      <label htmlFor={`color-${label}`}>{label}</label>
+      <div className="color-input-wrapper">
+        <input className="color-input" name={`color-${label}`} type="color" {...rest} />
+      </div>
+    </div>
+  )
+}

+ 21 - 0
tldraw/apps/tldraw-logseq/src/components/inputs/SwitchInput.tsx

@@ -0,0 +1,21 @@
+import * as React from 'react'
+import * as Switch from '@radix-ui/react-switch'
+interface SwitchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
+  label: string
+  onCheckedChange: (checked: boolean) => void
+}
+
+export function SwitchInput({ label, ...rest }: SwitchInputProps) {
+  return (
+    <div className="input">
+      <label htmlFor={`switch-${label}`}>{label}</label>
+      <Switch.Root
+        className="switch-input-root"
+        checked={rest.checked}
+        onCheckedChange={rest.onCheckedChange}
+      >
+        <Switch.Thumb className="switch-input-thumb" />
+      </Switch.Root>
+    </div>
+  )
+}

+ 0 - 7
tldraw/apps/tldraw-logseq/src/index.ts

@@ -1,8 +1 @@
-// export * as shapes from '~lib/shapes'
-// export * as tools from '~lib/tools'
-// export { AppUI } from '~components/AppUI'
-// export { ContextBar } from '~components/ContextBar/ContextBar'
-// export { AppCanvas, AppProvider } from '@tldraw/react'
-// export { useFileDrop } from '~hooks/useFileDrop'
-
 export * from './app'

+ 76 - 25
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -1,10 +1,12 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
-import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
 import { MagnifyingGlassIcon } from '@radix-ui/react-icons'
+import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
+import { HTMLContainer, TLComponentProps, TLContextBarProps, useApp } from '@tldraw/react'
 import { makeObservable, transaction } from 'mobx'
 import { observer } from 'mobx-react-lite'
 import * as React from 'react'
+import { ColorInput } from '~components/inputs/ColorInput'
+import { SwitchInput } from '~components/inputs/SwitchInput'
 import { useCameraMovingRef } from '~hooks/useCameraMoving'
 import type { Shape } from '~lib'
 import { LogseqContext } from '~lib/logseq-context'
@@ -13,6 +15,8 @@ import { CustomStyleProps, withClampedStyles } from './style-props'
 export interface LogseqPortalShapeProps extends TLBoxShapeProps, CustomStyleProps {
   type: 'logseq-portal'
   pageId: string // page name or UUID
+  collapsed: boolean
+  collapsedHeight: number
 }
 
 interface LogseqQuickSearchProps {
@@ -97,11 +101,13 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     parentId: 'page',
     point: [0, 0],
     size: [600, 50],
-    stroke: 'transparent',
+    collapsedHeight: 0,
+    stroke: 'var(--ls-primary-text-color)',
     fill: 'var(--ls-secondary-background-color)',
     strokeWidth: 2,
     opacity: 1,
     pageId: '',
+    collapsed: false,
   }
 
   hideRotateHandle = true
@@ -110,11 +116,53 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   canActivate = true
   canEdit = true
 
+  constructor(props = {} as Partial<LogseqPortalShapeProps>) {
+    super(props)
+    makeObservable(this)
+    if (props.collapsed) {
+      this.canResize = [true, false]
+    }
+  }
+
   ReactContextBar = observer(() => {
-    return <>123</>
+    return (
+      <>
+        <ColorInput
+          label="Background"
+          value={this.props.fill}
+          onChange={e => {
+            this.update({
+              fill: e.target.value,
+            })
+          }}
+        />
+        <ColorInput
+          label="Text"
+          value={this.props.stroke}
+          onChange={e => {
+            this.update({
+              stroke: e.target.value,
+            })
+          }}
+        />
+        <SwitchInput
+          label="Collapsed"
+          checked={this.props.collapsed}
+          onCheckedChange={collapsing => {
+            const originalHeight = this.props.size[1]
+            this.canResize[1] = !collapsing
+            this.update({
+              collapsed: collapsing,
+              size: [this.props.size[0], collapsing ? 40 : this.props.collapsedHeight],
+              collapsedHeight: collapsing ? originalHeight : this.props.collapsedHeight,
+            })
+          }}
+        />
+      </>
+    )
   })
 
-  ReactComponent = observer(({ events, isErasing, isActivated }: TLComponentProps) => {
+  ReactComponent = observer(({ events, isErasing, isActivated, isBinding }: TLComponentProps) => {
     const {
       props: { opacity, pageId, strokeWidth, stroke, fill },
     } = this
@@ -171,12 +219,13 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
             <LogseqQuickSearch onChange={commitChange} />
           ) : (
             <div
-              className="shadow-xl tl-logseq-portal-container"
+              className="tl-logseq-portal-container"
               style={{
                 background: fill,
-                boxShadow: isActivated
-                  ? '0px 0px 0 var(--tl-binding-distance) var(--tl-binding)'
-                  : '',
+                boxShadow:
+                  isActivated || isBinding
+                    ? '0px 0px 0 var(--tl-binding-distance) var(--tl-binding)'
+                    : 'var(--shadow-large)',
                 opacity: isSelected ? 0.8 : 1,
               }}
             >
@@ -184,26 +233,28 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
                 <span className="text-xs rounded border mr-2 px-1">P</span>
                 {pageId}
               </div>
-              <div
-                style={{
-                  width: '100%',
-                  overflow: 'auto',
-                  borderRadius: '8px',
-                  overscrollBehavior: 'none',
-                  // height: '100%',
-                  flex: 1,
-                }}
-              >
+              {!this.props.collapsed && (
                 <div
                   style={{
-                    padding: '12px',
-                    height: '100%',
-                    cursor: 'default',
+                    width: '100%',
+                    overflow: 'auto',
+                    borderRadius: '8px',
+                    overscrollBehavior: 'none',
+                    // height: '100%',
+                    flex: 1,
                   }}
                 >
-                  <Page pageId={pageId} />
+                  <div
+                    style={{
+                      padding: '12px',
+                      height: '100%',
+                      cursor: 'default',
+                    }}
+                  >
+                    <Page pageId={pageId} />
+                  </div>
                 </div>
-              </div>
+              )}
             </div>
           )}
         </div>
@@ -223,7 +274,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   validateProps = (props: Partial<LogseqPortalShapeProps>) => {
     if (props.size !== undefined) {
       props.size[0] = Math.max(props.size[0], 50)
-      props.size[1] = Math.max(props.size[1], 50)
+      props.size[1] = Math.max(props.size[1], 40)
     }
     return withClampedStyles(props)
   }

+ 1 - 0
tldraw/apps/tldraw-logseq/src/lib/tools/LineTool.tsx

@@ -2,6 +2,7 @@ import { TLLineTool } from '@tldraw/core'
 import type { TLReactEventMap } from '@tldraw/react'
 import { Shape, LineShape } from '~lib'
 
+// @ts-expect-error maybe later
 export class LineTool extends TLLineTool<LineShape, Shape, TLReactEventMap> {
   static id = 'line'
   static shortcut = ['l']

+ 68 - 31
tldraw/apps/tldraw-logseq/src/styles.css

@@ -2,14 +2,17 @@
 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap');
 
 .logseq-tldraw {
-  --color-panel: var(--ls-secondary-background-color);
+  --color-panel: var(--ls-tertiary-background-color);
+  --color-panel-inverted: var(--ls-secondary-text-color);
   --color-text: var(--ls-primary-text-color);
-  --color-hover: var(--ls-tertiary-background-color);
+  --color-text-inverted: var(--ls-tertiary-background-color);
+  --color-hover: var(--ls-secondary-background-color);
   --color-selectedStroke: rgb(42, 123, 253);
   --color-selectedFill: rgba(66, 133, 244);
-  --color-selectedContrast: #ffffff;
-  --shadow-medium: 0px 0px 16px -1px rgba(0, 0, 0, 0.05), 0px 0px 16px -8px rgba(0, 0, 0, 0.09),
-    0px 0px 16px -12px rgba(0, 0, 0, 0.2);
+  --color-selectedContrast: #fff;
+  --shadow-small: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+  --shadow-medium: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+  --shadow-large: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
 }
 
 .logseq-tldraw-wrapper {
@@ -20,13 +23,14 @@
   flex-direction: column;
 }
 
-.logseq-tldraw label {
-  font-family: 'Inter', Arial, Helvetica, sans-serif;
+.logseq-tldraw .contextbar label {
+  font-family: var(--ls-font-family);
+  font-size: 10px;
 }
 
 .logseq-tldraw button {
   font-size: 13px;
-  font-family: 'Inter', Arial, Helvetica, sans-serif;
+  font-family: var(--ls-font-family);
   background: none;
   border: none;
   cursor: pointer;
@@ -54,14 +58,14 @@
 .logseq-tldraw .contextbar {
   pointer-events: all;
   position: relative;
-  background-color: var(--color-panel);
+  background-color: #fff;
   color: var(--color-text);
   padding: 8px 12px;
   border-radius: 8px;
   white-space: nowrap;
   display: flex;
-  gap: 4px;
-  align-items: center;
+  gap: 12px;
+  align-items: stretch;
   font-size: 14px;
   will-change: transform, contents;
   box-shadow: var(--shadow-medium);
@@ -99,11 +103,46 @@
   padding: 2px;
 }
 
-.logseq-tldraw .color-input {
-  height: 24px;
-  padding: 0 2px;
-  background: none;
+.logseq-tldraw .color-input-wrapper {
+  overflow: hidden;
+  height: 18px;
+  width: 46px;
   border-radius: 2px;
+  margin: 2px;
+  box-shadow: 0 0 0 2px var(--ls-tertiary-background-color);
+}
+
+.logseq-tldraw .color-input {
+  transform: translate(-4px, -4px) scale(1.5);
+}
+
+.logseq-tldraw .switch-input-root {
+  all: unset;
+  width: 36px;
+  height: 20px;
+  background-color: rgba(0, 0, 0, 0.44);
+  position: relative;
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+  border-radius: 9999px;
+}
+
+.logseq-tldraw .switch-input-root[data-state='checked'] {
+  background: var(--ls-active-primary-color);
+}
+
+.logseq-tldraw .switch-input-thumb {
+  display: block;
+  width: 16px;
+  height: 16px;
+  background-color: white;
+  border-radius: 9999px;
+  transition: transform 100ms ease 0s;
+  transform: translateX(3px);
+  will-change: transform;
+}
+
+.logseq-tldraw .switch-input-thumb[data-state='checked'] {
+  transform: translateX(17px);
 }
 
 .logseq-tldraw .text-input {
@@ -167,7 +206,7 @@
   align-items: center;
   justify-content: center;
   font-size: 13px;
-  font-family: 'Inter', Arial, Helvetica, sans-serif;
+  font-family: var(--ls-font-family);
   background: none;
   border: none;
   cursor: pointer;
@@ -200,21 +239,18 @@
   position: absolute;
   white-space: pre-wrap;
   overflow-wrap: break-word;
-  width: auto;
   border: 1px solid transparent;
-  margin: 0px;
-  padding: 0px;
+  margin: 0;
+  padding: 0;
   z-index: 9999;
   user-select: none;
   width: 100%;
   height: 100%;
-  z-index: 1;
   min-height: 1;
   min-width: 1;
   line-height: 1;
   outline: 0;
   backface-visibility: hidden;
-  user-select: none;
   pointer-events: all;
   vertical-align: baseline;
   -webkit-user-drag: none;
@@ -295,8 +331,8 @@
 
 .logseq-tldraw .text-label-wrapper {
   position: absolute;
-  top: 0px;
-  left: 0px;
+  top: 0;
+  left: 0;
   width: 100%;
   height: 100%;
   display: flex;
@@ -313,7 +349,7 @@
   min-height: 1px;
   min-width: 1px;
   line-height: 1;
-  outline: 0px;
+  outline: 0;
   font-weight: 500;
   text-align: center;
   backface-visibility: hidden;
@@ -324,8 +360,8 @@
 
 .logseq-tldraw .text-label-textarea {
   position: absolute;
-  top: 0px;
-  left: 0px;
+  top: 0;
+  left: 0;
   z-index: 1;
   width: 100%;
   height: 100%;
@@ -337,7 +373,7 @@
   min-width: inherit;
   line-height: inherit;
   letter-spacing: inherit;
-  outline: 0px;
+  outline: 0;
   font-weight: inherit;
   overflow: hidden;
   backface-visibility: hidden;
@@ -404,6 +440,7 @@
   background-color: var(--ls-secondary-background-color);
   padding: 4px 12px;
   border-radius: 8px;
+  box-shadow: var(--shadow-small);
 }
 
 .logseq-tldraw .tl-quick-search-input-sizer {
@@ -417,6 +454,7 @@
 .logseq-tldraw .tl-quick-search-input {
   grid-area: 1/2;
   width: auto;
+  line-height: 1;
 }
 
 .logseq-tldraw .tl-quick-search-input-sizer::after {
@@ -433,14 +471,13 @@
   left: 0;
   background-color: var(--ls-primary-background-color);
   max-height: 300px;
+  min-width: 320px;
+  box-shadow: var(--shadow-large);
   width: 100%;
   overflow-y: auto;
   display: flex;
   flex-direction: column;
   border-radius: 8px;
-  --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
-  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
-    var(--tw-shadow);
   overscroll-behavior: none;
 }
 
@@ -475,7 +512,7 @@
   background: transparent;
   display: flex;
   color: var(--ls-title-text-color);
-  padding: 0px 1rem;
+  padding: 0 1rem;
   align-items: center;
 }
 

+ 1 - 1
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -655,7 +655,7 @@ export class TLApp<
         'select.pointingRotateHandle',
         'select.pointingResizeHandle'
       ) &&
-      selectedShapesArray.length > 0 &&
+      selectedShapesArray.length === 1 &&
       !selectedShapesArray.every(shape => shape.hideResizeHandles)
     )
   }

+ 1 - 1
tldraw/packages/core/src/lib/shapes/TLShape/TLShape.tsx

@@ -97,7 +97,7 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
   // Behavior options
   canChangeAspectRatio: TLFlag = true
   canUnmount: TLFlag = true
-  canResize: TLFlag = true
+  canResize: [TLFlag, TLFlag] = [true, true]
   canScale: TLFlag = true
   canFlip: TLFlag = true
   canEdit: TLFlag = false

+ 27 - 24
tldraw/packages/core/src/lib/tools/TLSelectTool/states/ResizingState.ts

@@ -66,28 +66,30 @@ export class ResizingState<
     this.initialCommonBounds = { ...selectionBounds }
     // @ts-expect-error maybe later
     this.snapshots = Object.fromEntries(
-      selectedShapesArray.filter(s => !s.draft).map(shape => {
-        const bounds = { ...shape.bounds }
-        const [cx, cy] = BoundsUtils.getBoundsCenter(bounds)
-        return [
-          shape.id,
-          {
-            props: shape.serialized,
-            bounds,
-            transformOrigin: [
-              (cx - this.initialCommonBounds.minX) / this.initialCommonBounds.width,
-              (cy - this.initialCommonBounds.minY) / this.initialCommonBounds.height,
-            ],
-            innerTransformOrigin: [
-              (cx - initialInnerBounds.minX) / initialInnerBounds.width,
-              (cy - initialInnerBounds.minY) / initialInnerBounds.height,
-            ],
-            isAspectRatioLocked:
-              shape.props.isAspectRatioLocked ||
-              Boolean(!shape.canChangeAspectRatio || shape.props.rotation),
-          },
-        ]
-      })
+      selectedShapesArray
+        .filter(s => !s.draft)
+        .map(shape => {
+          const bounds = { ...shape.bounds }
+          const [cx, cy] = BoundsUtils.getBoundsCenter(bounds)
+          return [
+            shape.id,
+            {
+              props: shape.serialized,
+              bounds,
+              transformOrigin: [
+                (cx - this.initialCommonBounds.minX) / this.initialCommonBounds.width,
+                (cy - this.initialCommonBounds.minY) / this.initialCommonBounds.height,
+              ],
+              innerTransformOrigin: [
+                (cx - initialInnerBounds.minX) / initialInnerBounds.width,
+                (cy - initialInnerBounds.minY) / initialInnerBounds.height,
+              ],
+              isAspectRatioLocked:
+                shape.props.isAspectRatioLocked ||
+                Boolean(!shape.canChangeAspectRatio || shape.props.rotation),
+            },
+          ]
+        })
     )
     selectedShapesArray.forEach(shape => shape.onResizeStart?.({ isSingle: this.isSingle }))
   }
@@ -163,8 +165,9 @@ export class ResizingState<
         scaleX < 0,
         scaleY < 0
       )
+      const canResizeAny = shape.canResize.some(r => r)
       // If the shape can't resize and it's the only shape selected, bail
-      if (!(shape.canResize || shape.props.isSizeLocked) && this.isSingle) {
+      if (!(canResizeAny || shape.props.isSizeLocked) && this.isSingle) {
         return
       }
       let scale = [scaleX, scaleY]
@@ -183,7 +186,7 @@ export class ResizingState<
         rotation *= -1
       }
       // If the shape is aspect ratio locked or size locked...
-      if (isAspectRatioLocked || !shape.canResize || shape.props.isSizeLocked) {
+      if (isAspectRatioLocked || !canResizeAny || shape.props.isSizeLocked) {
         relativeBounds.width = initialShapeBounds.width
         relativeBounds.height = initialShapeBounds.height
         if (isAspectRatioLocked) {

+ 2 - 1
tldraw/packages/core/src/utils/BoundsUtils.ts

@@ -323,7 +323,8 @@ export class BoundsUtils {
     handle: TLResizeCorner | TLResizeEdge | 'center',
     delta: number[],
     rotation = 0,
-    isAspectRatioLocked = false
+    isAspectRatioLocked = false,
+    [canResizeX, canResizeY] = [true, true]
   ): TLBounds & { scaleX: number; scaleY: number } {
     // Create top left and bottom right corners.
     const [ax0, ay0] = [bounds.minX, bounds.minY]

+ 43 - 32
tldraw/packages/react/src/components/ui/SelectionForeground/SelectionForeground.tsx

@@ -12,12 +12,15 @@ export const SelectionForeground = observer(function SelectionForeground<S exten
   zoom,
   showResizeHandles,
   showRotateHandles,
+  shapes,
 }: TLSelectionComponentProps<S>) {
   const { width, height } = bounds
 
   const size = 8 / zoom
   const targetSize = 6 / zoom
 
+  const canResize = shapes.length === 1 ? shapes[0].canResize : [true, true]
+
   return (
     <SVGContainer>
       <rect
@@ -33,6 +36,7 @@ export const SelectionForeground = observer(function SelectionForeground<S exten
         height={0}
         targetSize={targetSize}
         edge={TLResizeEdge.Top}
+        disabled={!canResize[1]}
         isHidden={!showResizeHandles}
       />
       <EdgeHandle
@@ -42,6 +46,7 @@ export const SelectionForeground = observer(function SelectionForeground<S exten
         height={height - targetSize * 4}
         targetSize={targetSize}
         edge={TLResizeEdge.Right}
+        disabled={!canResize[0]}
         isHidden={!showResizeHandles}
       />
       <EdgeHandle
@@ -51,6 +56,7 @@ export const SelectionForeground = observer(function SelectionForeground<S exten
         height={0}
         targetSize={targetSize}
         edge={TLResizeEdge.Bottom}
+        disabled={!canResize[1]}
         isHidden={!showResizeHandles}
       />
       <EdgeHandle
@@ -60,6 +66,7 @@ export const SelectionForeground = observer(function SelectionForeground<S exten
         height={height - targetSize * 4}
         targetSize={targetSize}
         edge={TLResizeEdge.Left}
+        disabled={!canResize[0]}
         isHidden={!showResizeHandles}
       />
       <RotateCornerHandle
@@ -90,38 +97,42 @@ export const SelectionForeground = observer(function SelectionForeground<S exten
         corner={TLRotateCorner.BottomLeft}
         isHidden={!showRotateHandles}
       />
-      <CornerHandle
-        cx={0}
-        cy={0}
-        size={size}
-        targetSize={targetSize}
-        corner={TLResizeCorner.TopLeft}
-        isHidden={!showResizeHandles}
-      />
-      <CornerHandle
-        cx={width}
-        cy={0}
-        size={size}
-        targetSize={targetSize}
-        corner={TLResizeCorner.TopRight}
-        isHidden={!showResizeHandles}
-      />
-      <CornerHandle
-        cx={width}
-        cy={height}
-        size={size}
-        targetSize={targetSize}
-        corner={TLResizeCorner.BottomRight}
-        isHidden={!showResizeHandles}
-      />
-      <CornerHandle
-        cx={0}
-        cy={height}
-        size={size}
-        targetSize={targetSize}
-        corner={TLResizeCorner.BottomLeft}
-        isHidden={!showResizeHandles}
-      />
+      {canResize?.every(r => r) && (
+        <>
+          <CornerHandle
+            cx={0}
+            cy={0}
+            size={size}
+            targetSize={targetSize}
+            corner={TLResizeCorner.TopLeft}
+            isHidden={!showResizeHandles}
+          />
+          <CornerHandle
+            cx={width}
+            cy={0}
+            size={size}
+            targetSize={targetSize}
+            corner={TLResizeCorner.TopRight}
+            isHidden={!showResizeHandles}
+          />
+          <CornerHandle
+            cx={width}
+            cy={height}
+            size={size}
+            targetSize={targetSize}
+            corner={TLResizeCorner.BottomRight}
+            isHidden={!showResizeHandles}
+          />
+          <CornerHandle
+            cx={0}
+            cy={height}
+            size={size}
+            targetSize={targetSize}
+            corner={TLResizeCorner.BottomLeft}
+            isHidden={!showResizeHandles}
+          />
+        </>
+      )}
       {/* {showRotateHandles && (
         <RotateHandle cx={width / 2} cy={0 - targetSize * 2} size={size} targetSize={targetSize} />
       )} */}

+ 3 - 1
tldraw/packages/react/src/components/ui/SelectionForeground/handles/EdgeHandle.tsx

@@ -18,6 +18,7 @@ interface EdgeHandleProps {
   targetSize: number
   edge: TLResizeEdge
   isHidden?: boolean
+  disabled?: boolean
 }
 
 export const EdgeHandle = observer<EdgeHandleProps>(function EdgeHandle({
@@ -27,13 +28,14 @@ export const EdgeHandle = observer<EdgeHandleProps>(function EdgeHandle({
   height,
   targetSize,
   edge,
+  disabled,
   isHidden,
 }: EdgeHandleProps): JSX.Element {
   const events = useBoundsEvents(edge)
 
   return (
     <rect
-      pointerEvents={isHidden ? 'none' : 'all'}
+      pointerEvents={(isHidden || disabled) ? 'none' : 'all'}
       className={'tl-transparent tl-edge-handle ' + (isHidden ? '' : edgeClassnames[edge])}
       aria-label={`${edge} target`}
       opacity={isHidden ? 0 : 1}

+ 2 - 0
tldraw/packages/react/src/hooks/useStylesheet.ts

@@ -152,6 +152,7 @@ const tlcss = css`
     cursor: var(--tl-cursor) !important;
     box-sizing: border-box;
     color: var(--tl-foreground);
+    willChange: transform;
   }
 
   .tl-overlay {
@@ -208,6 +209,7 @@ const tlcss = css`
     left: 0px;
     transform-origin: center center;
     contain: layout style size;
+    willChange: transform;
   }
 
   .tl-positioned {

+ 107 - 0
tldraw/yarn.lock

@@ -1817,11 +1817,118 @@
   dependencies:
     "@octokit/openapi-types" "^11.2.0"
 
+"@radix-ui/[email protected]":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-0.1.0.tgz#6206b97d379994f0d1929809db035733b337e543"
+  integrity sha512-tqxZKybwN5Fa3VzZry4G6mXAAb9aAqKmPtnVbZpL0vsBwvOHTBwsjHVPXylocYLwEtBY9SCe665bYnNB515uoA==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+
+"@radix-ui/[email protected]":
+  version "0.1.1-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-0.1.1-rc.40.tgz#3887e806b9bdb71ec5839ce4246e10517101204a"
+  integrity sha512-tN14wn9XxR7r2b1Klh9FQsWch4DojMudesSc7YQexl/54F6fmSXBqJQQyDO3hsNbiWyyco2MK8wxDpmxX5YWmw==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+
+"@radix-ui/[email protected]":
+  version "0.1.2-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-0.1.2-rc.40.tgz#6c7ade9b3d0177bf555da41a5cf3910c4e5bb154"
+  integrity sha512-Jv6HE3RypVP9S1FlLyswxx42b+MVDhIS+/BUTWAr4941vweQVhVRfNThpBgHKcxC4Sk5dUyBhn3oYJImSQDskw==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+
 "@radix-ui/react-icons@^1.1.1":
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.1.1.tgz#38d2aa548035dd3b799c169bd17177b1cec3152b"
   integrity sha512-xc3wQC59rsFylVbSusQCrrM+6695ppF730Q6yqzhRdqDcRNWIm2R6ngpzBoSOQMcwnq4p805F+Gr7xo4fmtN1A==
 
+"@radix-ui/[email protected]":
+  version "0.1.6-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-0.1.6-rc.40.tgz#f14acf42d26ef92cd84ead3323e5b97caae6f29a"
+  integrity sha512-ohpMxiZRde3RQ+SYFcqveIZSXPQLhPh+DdhJZf79EVXchN0hxevIPgjNV1QK711FNmKy0NeJePoVB6WYiNSRgQ==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+    "@radix-ui/react-use-layout-effect" "0.1.1-rc.40"
+
+"@radix-ui/[email protected]":
+  version "0.1.6-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-0.1.6-rc.40.tgz#e691007f9f6ec43162c964c0538257bc23c8e61b"
+  integrity sha512-3ADQzhz9mmItDvsrLKqAN+5HVP6DPiExHixO7XO7V1o1+9vliAR2DB0W70NmSrQutQwHQJi2Z6MrKbB75DIsjg==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+    "@radix-ui/react-compose-refs" "0.1.1-rc.40"
+    "@radix-ui/react-context" "0.1.2-rc.40"
+    "@radix-ui/react-id" "0.1.6-rc.40"
+    "@radix-ui/react-primitive" "0.1.5-rc.40"
+
+"@radix-ui/[email protected]":
+  version "0.1.5-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-0.1.5-rc.40.tgz#aa51bfa2332fa5f1a35f331a0f86d14c0be9f07d"
+  integrity sha512-eUQC5INhR31OSM4eGcsZ/30IW10ESqmNELCCElw5QoPncGdWjvj3KSfC+MIQIYw8LEbOoOWDmM/oMenAgdHh9A==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+    "@radix-ui/react-slot" "0.1.3-rc.40"
+
+"@radix-ui/[email protected]":
+  version "0.1.3-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-0.1.3-rc.40.tgz#4f3877647132d964ed6b20c3a96e26f0dba6c01d"
+  integrity sha512-cj91yn3VirG81ZDGL9plF7qosAHKsow1Ve8zwokFneYF3poeS7+IbUAHBE2+K8nOEK0c25OD3R2jQ5tvTYLPJQ==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+    "@radix-ui/react-compose-refs" "0.1.1-rc.40"
+
+"@radix-ui/react-switch@^0.1.6-rc.40":
+  version "0.1.6-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-0.1.6-rc.40.tgz#9975eace45030be84c54349b1f66dd0e80404e68"
+  integrity sha512-NTVH1z7iqqyoQHpH9ez4ErAyPY2OsO6m13G7cpRGFrr7TF8r/1k4kFMTUNp/6Bds2HEa+HBIcDfCFkEb2QIm8g==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+    "@radix-ui/primitive" "0.1.0"
+    "@radix-ui/react-compose-refs" "0.1.1-rc.40"
+    "@radix-ui/react-context" "0.1.2-rc.40"
+    "@radix-ui/react-label" "0.1.6-rc.40"
+    "@radix-ui/react-primitive" "0.1.5-rc.40"
+    "@radix-ui/react-use-controllable-state" "0.1.1-rc.40"
+    "@radix-ui/react-use-previous" "0.1.2-rc.40"
+    "@radix-ui/react-use-size" "0.1.2-rc.40"
+
+"@radix-ui/[email protected]":
+  version "0.1.1-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-0.1.1-rc.40.tgz#e1d6d950c9dae36218968eb73c60e2b649117ecc"
+  integrity sha512-IzvrTip7gEp56VemxwDzmb7AgOMzgDAy1SDUnwBH0qpP5BKTXUKumgAGY+W8rNcZmUcA7EmE8wW9kbf+NAgSiw==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+
+"@radix-ui/[email protected]":
+  version "0.1.1-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.1.1-rc.40.tgz#2d956f2bae3dd39744779f4521dd11451d0948d7"
+  integrity sha512-03FXpyzi+ql5Bnqi6Ues6fClbrFlzttqqxtm/E2ZK9B9e8qT3ArI5Vg0ZtVBHWWtC+96YJao5zcJXk1cSOBiMg==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+    "@radix-ui/react-use-callback-ref" "0.1.1-rc.40"
+
+"@radix-ui/[email protected]":
+  version "0.1.1-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.1-rc.40.tgz#14368315d14fe7ee13b91c9bef81e36ebf06b8f4"
+  integrity sha512-yv/LziHeaiJf9WGRgBcQb9+v3IGChwtEWY8taUN1MMAxVw2M2IqRrW+pZkgClD2qe3Axukl2BXAm2h/+yvCTiQ==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+
+"@radix-ui/[email protected]":
+  version "0.1.2-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-0.1.2-rc.40.tgz#36647d5b9078485c87ae1212fbcbbfed4edcb971"
+  integrity sha512-gRh3c9Ua20zTa+QXebcFmZQ0Oj5lib41EYuFE7h/qxLA5mkVcp6DKvgMvNOaHkcfa3MfPBGLZ1zdM1rcEl1Jow==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+
+"@radix-ui/[email protected]":
+  version "0.1.2-rc.40"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-0.1.2-rc.40.tgz#8f8b887d180219e082ae465b7ba4496cdabb5612"
+  integrity sha512-G+0CGeZI+mlGAFHl21SKucj24397a5dvJz2wTjVEz10FhPuoVZGN1etOuFSU/Rvsr5hv6g5UvD1iOkskPG9DeA==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+
 "@rollup/pluginutils@^4.2.1":
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"