Преглед изворни кода

Merge branch 'whiteboards' into whiteboards

Peng Xiao пре 3 година
родитељ
комит
17344f6408
41 измењених фајлова са 888 додато и 232 уклоњено
  1. 1 1
      src/main/frontend/components/settings.css
  2. 0 1
      src/main/frontend/components/whiteboard.css
  3. 2 1
      tldraw/apps/tldraw-logseq/package.json
  4. 7 4
      tldraw/apps/tldraw-logseq/src/app.tsx
  5. 1 1
      tldraw/apps/tldraw-logseq/src/components/AppUI.tsx
  6. 2 0
      tldraw/apps/tldraw-logseq/src/components/Devtools/Devtools.tsx
  7. 1 1
      tldraw/apps/tldraw-logseq/src/components/StatusBar/StatusBar.tsx
  8. 24 3
      tldraw/apps/tldraw-logseq/src/components/inputs/ColorInput.tsx
  9. 16 0
      tldraw/apps/tldraw-logseq/src/components/inputs/SelectInput.tsx
  10. 21 0
      tldraw/apps/tldraw-logseq/src/components/inputs/SwitchInput.tsx
  11. 140 0
      tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts
  12. 0 9
      tldraw/apps/tldraw-logseq/src/index.ts
  13. 1 1
      tldraw/apps/tldraw-logseq/src/lib/shapes/LineShape.tsx
  14. 163 70
      tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx
  15. 1 1
      tldraw/apps/tldraw-logseq/src/lib/shapes/PencilShape.tsx
  16. 1 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx
  17. 1 1
      tldraw/apps/tldraw-logseq/src/lib/shapes/arrow/Arrow.tsx
  18. 1 1
      tldraw/apps/tldraw-logseq/src/lib/shapes/arrow/arrowHelpers.ts
  19. 3 1
      tldraw/apps/tldraw-logseq/src/lib/tools/LineTool.tsx
  20. 193 37
      tldraw/apps/tldraw-logseq/src/styles.css
  21. 3 0
      tldraw/demo/postcss.config.js
  22. 46 1
      tldraw/demo/src/App.jsx
  23. 1 2
      tldraw/demo/src/main.jsx
  24. 2 4
      tldraw/demo/tailwind.config.js
  25. 33 2
      tldraw/packages/core/src/lib/TLApp/TLApp.ts
  26. 9 3
      tldraw/packages/core/src/lib/shapes/TLShape/TLShape.tsx
  27. 3 1
      tldraw/packages/core/src/lib/tools/TLDotTool/states/CreatingState.tsx
  28. 1 0
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/BrushingState.ts
  29. 4 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts
  30. 1 0
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingCanvasState.ts
  31. 27 24
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/ResizingState.ts
  32. 4 0
      tldraw/packages/core/src/types/types.ts
  33. 2 1
      tldraw/packages/core/src/utils/BoundsUtils.ts
  34. 43 32
      tldraw/packages/react/src/components/ui/SelectionForeground/SelectionForeground.tsx
  35. 3 1
      tldraw/packages/react/src/components/ui/SelectionForeground/handles/EdgeHandle.tsx
  36. 0 20
      tldraw/packages/react/src/hooks/useCanvasEvents.ts
  37. 10 1
      tldraw/packages/react/src/hooks/useKeyboardEvents.ts
  38. 2 0
      tldraw/packages/react/src/hooks/useSetup.ts
  39. 7 4
      tldraw/packages/react/src/hooks/useStylesheet.ts
  40. 1 0
      tldraw/packages/react/src/types/TLReactSubscriptions.tsx
  41. 107 0
      tldraw/yarn.lock

+ 1 - 1
src/main/frontend/components/settings.css

@@ -116,7 +116,7 @@
 
         &.app-updater {
           padding-top: 15px;
-          align-items: start;
+          align-items: flex-start;
 
 
           > .wrap {

+ 0 - 1
src/main/frontend/components/whiteboard.css

@@ -1,3 +1,2 @@
 .logseq-tldraw .tl-container {
-  background-color: var(--ls-secondary-background-color);
 }

+ 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",

+ 7 - 4
tldraw/apps/tldraw-logseq/src/app.tsx

@@ -11,6 +11,7 @@ import * as React from 'react'
 import { AppUI } from '~components/AppUI'
 import { ContextBar } from '~components/ContextBar/ContextBar'
 import { useFileDrop } from '~hooks/useFileDrop'
+import { usePaste } from '~hooks/usePaste'
 import { LogseqContext } from '~lib/logseq-context'
 import { Shape, shapes } from '~lib/shapes'
 import {
@@ -32,14 +33,14 @@ const components: TLReactComponents<Shape> = {
 }
 
 const tools: TLReactToolConstructor<Shape>[] = [
-  BoxTool,
-  DotTool,
-  EllipseTool,
+  // BoxTool,
+  // DotTool,
+  // EllipseTool,
+  // PolygonTool,
   NuEraseTool,
   HighlighterTool,
   LineTool,
   PencilTool,
-  PolygonTool,
   TextTool,
   YouTubeTool,
   LogseqPortalTool,
@@ -55,6 +56,7 @@ interface LogseqTldrawProps {
 
 export const App = function App(props: LogseqTldrawProps): JSX.Element {
   const onFileDrop = useFileDrop()
+  const onPaste = usePaste()
 
   const Page = React.useMemo(() => React.memo(props.PageComponent), [])
 
@@ -65,6 +67,7 @@ export const App = function App(props: LogseqTldrawProps): JSX.Element {
         Shapes={shapes}
         Tools={tools}
         onFileDrop={onFileDrop}
+        onPaste={onPaste}
         {...props}
       >
         <div className="logseq-tldraw logseq-tldraw-wrapper">

+ 1 - 1
tldraw/apps/tldraw-logseq/src/components/AppUI.tsx

@@ -12,7 +12,7 @@ const isDev = process.env.NODE_ENV === 'development'
 export const AppUI = observer(function AppUI() {
   return (
     <>
-    {/* <ToolBar /> */}
+      {/* <ToolBar /> */}
       <Minimap />
       {isDev && <StatusBar />}
       {isDev && <DevTools />}

+ 2 - 0
tldraw/apps/tldraw-logseq/src/components/Devtools/Devtools.tsx

@@ -51,6 +51,8 @@ export const DevTools = observer(() => {
         <div
           style={{
             flex: 1,
+            display: 'flex',
+            alignItems: 'center',
           }}
         >
           {rendererStatusText}

+ 1 - 1
tldraw/apps/tldraw-logseq/src/components/StatusBar/StatusBar.tsx

@@ -17,7 +17,7 @@ export const StatusBar = observer(function StatusBar() {
     <div className="statusbar">
       {app.selectedTool.id} | {app.selectedTool.currentState.id}
       <div style={{ flex: 1 }} />
-      <div id="tl-statusbar-anchor" />
+      <div id="tl-statusbar-anchor" style={{ display: 'flex' }} />
     </div>
   )
 })

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

@@ -4,11 +4,32 @@ interface ColorInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
   label: string
 }
 
-export function ColorInput({ label, ...rest }: ColorInputProps) {
+export function ColorInput({ label, value, ...rest }: ColorInputProps) {
+  const ref = React.useRef<HTMLDivElement>(null)
+  const [computedValue, setComputedValue] = React.useState(value)
+
+  // TODO: listen to theme change?
+  React.useEffect(() => {
+    if (value?.toString().startsWith('var') && ref.current) {
+      const varName = /var\((.*)\)/.exec(value.toString())?.[1]
+      if (varName) {
+        setComputedValue(getComputedStyle(ref.current).getPropertyValue(varName).trim())
+      }
+    }
+  }, [value])
+
   return (
-    <div className="input">
+    <div className="input" ref={ref}>
       <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"
+          value={computedValue}
+          {...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>
+  )
+}

+ 140 - 0
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -0,0 +1,140 @@
+import {
+  BoundsUtils,
+  fileToBase64,
+  getSizeFromSrc,
+  TLAsset,
+  TLBinding,
+  TLShapeModel,
+  uniqueId,
+} from '@tldraw/core'
+import type { TLReactCallbacks } from '@tldraw/react'
+import * as React from 'react'
+import type { Shape } from '~lib'
+
+export function usePaste() {
+  return React.useCallback<TLReactCallbacks<Shape>['onFileDrop']>(async (app, { point }) => {
+    const assetId = uniqueId()
+    interface ImageAsset extends TLAsset {
+      size: number[]
+    }
+
+    const assetsToCreate: ImageAsset[] = []
+    const shapesToCreate: TLShapeModel[] = []
+    const bindingsToCreate: TLBinding[] = []
+
+    async function handleImage(item: ClipboardItem) {
+      const firstImageType = item.types.find(type => type.startsWith('image'))
+      if (firstImageType) {
+        const blob = await item.getType(firstImageType)
+        const dataurl = await fileToBase64(blob)
+        if (typeof dataurl !== 'string') return false
+        const existingAsset = Object.values(app.assets).find(asset => asset.src === dataurl)
+        if (existingAsset) {
+          assetsToCreate.push(existingAsset as ImageAsset)
+          return false
+        }
+        // Create a new asset for this image
+        const asset: ImageAsset = {
+          id: assetId,
+          type: 'image',
+          src: dataurl,
+          size: await getSizeFromSrc(dataurl),
+        }
+        assetsToCreate.push(asset)
+        return true
+      }
+      return false
+    }
+
+    async function handleLogseqShapes(item: ClipboardItem) {
+      const plainTextType = item.types.find(type => type.startsWith('text/plain'))
+      if (plainTextType) {
+        const blob = await item.getType(plainTextType)
+        const rawText = await blob.text()
+        const data = JSON.parse(rawText)
+        if (data.type === 'logseq/whiteboard-shapes') {
+          const shapes = data.shapes as TLShapeModel[]
+          const commonBounds = BoundsUtils.getCommonBounds(
+            shapes.map(shape => ({
+              minX: shape.point?.[0] ?? point[0],
+              minY: shape.point?.[1] ?? point[1],
+              width: shape.size?.[0] ?? 4,
+              height: shape.size?.[1] ?? 4,
+              maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
+              maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
+            }))
+          )
+          const clonedShapes = shapes.map((shape: TLShapeModel) => {
+            return {
+              ...shape,
+              id: uniqueId(),
+              parentId: app.currentPageId,
+              point: [
+                point[0] + shape.point![0] - commonBounds.minX,
+                point[1] + shape.point![1] - commonBounds.minY,
+              ],
+            }
+          })
+          shapesToCreate.push(...clonedShapes)
+
+          // Try to rebinding the shapes to the new assets
+          shapesToCreate.forEach((s, idx) => {
+            if (s.handles) {
+              Object.values(s.handles).forEach(h => {
+                if (h.bindingId) {
+                  // try to bind the new shape
+                  const binding = app.currentPage.bindings[h.bindingId]
+                  // if the copied binding from/to is in the source
+                  const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
+                  const oldToIdx = shapes.findIndex(s => s.id === binding.toId)
+                  if (binding && oldFromIdx !== -1 && oldToIdx !== -1) {
+                    const newBinding: TLBinding = {
+                      ...binding,
+                      id: uniqueId(),
+                      fromId: shapesToCreate[oldFromIdx].id,
+                      toId: shapesToCreate[oldToIdx].id,
+                    }
+                    bindingsToCreate.push(newBinding)
+                    h.bindingId = newBinding.id
+                  } else {
+                    h.bindingId = undefined
+                  }
+                }
+              })
+            }
+          })
+        }
+      }
+    }
+
+    // TODO: supporting other pasting formats
+    for (const item of await navigator.clipboard.read()) {
+      try {
+        let handled = await handleImage(item)
+        if (!handled) {
+          await handleLogseqShapes(item)
+        }
+      } catch (error) {
+        console.error(error)
+      }
+    }
+
+    const allShapesToAdd: TLShapeModel[] = [
+      ...assetsToCreate.map((asset, i) => ({
+        id: uniqueId(),
+        type: 'image',
+        parentId: app.currentPageId,
+        point: [point[0] - asset.size[0] / 2 + i * 16, point[1] - asset.size[1] / 2 + i * 16],
+        size: asset.size,
+        assetId: asset.id,
+        opacity: 1,
+      })),
+      ...shapesToCreate,
+    ]
+    app.createAssets(assetsToCreate)
+    app.createShapes(allShapesToAdd)
+    app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
+
+    app.setSelectedShapes(allShapesToAdd.map(s => s.id))
+  }, [])
+}

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

@@ -1,10 +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'
-
-import './styles.css';
-
 export * from './app'

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/LineShape.tsx

@@ -30,7 +30,7 @@ export class LineShape extends TLLineShape<LineShapeProps> {
       start: { id: 'start', canBind: true, point: [0, 0] },
       end: { id: 'end', canBind: true, point: [1, 1] },
     },
-    stroke: '#000000',
+    stroke: 'var(--tl-foreground)',
     fill: '#ffffff',
     strokeWidth: 1,
     opacity: 1,

+ 163 - 70
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -1,18 +1,25 @@
 /* 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 { TLBounds, TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
+import { HTMLContainer, TLComponentProps, TLContextBarProps, useApp } from '@tldraw/react'
 import { makeObservable, transaction } from 'mobx'
+import { useGesture } from '@use-gesture/react'
 import { observer } from 'mobx-react-lite'
 import * as React from 'react'
-import { TextInput } from '~components/inputs/TextInput'
+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'
 import { CustomStyleProps, withClampedStyles } from './style-props'
 
+const HEADER_HEIGHT = 40
+
 export interface LogseqPortalShapeProps extends TLBoxShapeProps, CustomStyleProps {
   type: 'logseq-portal'
   pageId: string // page name or UUID
+  collapsed: boolean
+  collapsedHeight: number
 }
 
 interface LogseqQuickSearchProps {
@@ -24,63 +31,66 @@ const LogseqQuickSearch = observer(({ onChange }: LogseqQuickSearchProps) => {
   const rInput = React.useRef<HTMLInputElement>(null)
   const { search } = React.useContext(LogseqContext)
 
-  const secretPrefix = 'œ::'
-
   const commitChange = React.useCallback((id: string) => {
     setQ(id)
     onChange(id)
     rInput.current?.blur()
   }, [])
 
-  const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
-    const _q = e.currentTarget.value
-    if (_q.startsWith(secretPrefix)) {
-      const id = _q.substring(secretPrefix.length)
-      commitChange(id)
-    } else {
-      setQ(_q)
-    }
-  }, [])
-
   const options = React.useMemo(() => {
-    if (search && q) {
-      return search(q)
-    }
-    return null
+    return search?.(q)
   }, [search, q])
 
   React.useEffect(() => {
+    // autofocus seems not to be working
     setTimeout(() => {
       rInput.current?.focus()
     })
   }, [])
 
   return (
-    <>
-      <TextInput
-        ref={rInput}
-        label="Page name or block UUID"
-        type="text"
-        value={q}
-        onChange={handleChange}
-        onKeyDown={e => {
-          if (e.key === 'Enter') {
-            commitChange(q)
-          }
-        }}
-        list="logseq-portal-search-results"
-      />
-      <datalist id="logseq-portal-search-results">
-        {options?.map(option => (
-          <option key={option} value={secretPrefix + option}>
-            {option}
-          </option>
+    <div className="tl-quick-search">
+      <div className="tl-quick-search-input-container">
+        <MagnifyingGlassIcon className="tl-quick-search-icon" width={24} height={24} />
+        <div className="tl-quick-search-input-sizer" data-value={q}>
+          <input
+            ref={rInput}
+            type="text"
+            value={q}
+            placeholder="Search or create page"
+            onChange={q => setQ(q.target.value)}
+            onKeyDown={e => {
+              if (e.key === 'Enter') {
+                commitChange(q)
+              }
+            }}
+            className="tl-quick-search-input text-input"
+          />
+        </div>
+      </div>
+
+      <div className="tl-quick-search-options">
+        {options?.map(name => (
+          <div key={name} className="tl-quick-search-option" onClick={() => commitChange(name)}>
+            {name}
+          </div>
         ))}
-      </datalist>
-    </>
+      </div>
+    </div>
   )
 })
 
+const LogseqPortalShapeHeader = observer(
+  ({ type, pageId }: { type: 'P' | 'B'; pageId: string }) => {
+    return (
+      <div className="tl-logseq-portal-header">
+        <span className="type-tag">{type}</span>
+        {pageId}
+      </div>
+    )
+  }
+)
+
 export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   static id = 'logseq-portal'
   static smart = true
@@ -90,12 +100,15 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     type: 'logseq-portal',
     parentId: 'page',
     point: [0, 0],
-    size: [180, 75],
-    stroke: 'transparent',
+    size: [600, 50],
+    // collapsedHeight is the height before collapsing
+    collapsedHeight: 0,
+    stroke: 'var(--ls-primary-text-color)',
     fill: 'var(--ls-secondary-background-color)',
     strokeWidth: 2,
     opacity: 1,
     pageId: '',
+    collapsed: false,
   }
 
   hideRotateHandle = true
@@ -107,12 +120,52 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   constructor(props = {} as Partial<LogseqPortalShapeProps>) {
     super(props)
     makeObservable(this)
-    this.draft = true
+    if (props.collapsed) {
+      this.canResize = [true, false]
+    }
   }
 
-  ReactComponent = observer(({ events, isErasing, isActivated }: TLComponentProps) => {
+  ReactContextBar = observer(() => {
+    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 ? HEADER_HEIGHT : this.props.collapsedHeight],
+              collapsedHeight: collapsing ? originalHeight : this.props.collapsedHeight,
+            })
+          }}
+        />
+      </>
+    )
+  })
+
+  ReactComponent = observer(({ events, isErasing, isActivated, isBinding }: TLComponentProps) => {
     const {
-      props: { opacity, pageId, strokeWidth, stroke },
+      props: { opacity, pageId, stroke, fill },
     } = this
 
     const app = useApp<Shape>()
@@ -123,13 +176,32 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     const stop = React.useCallback(
       e => {
         if (!tlEventsEnabled) {
+          // TODO: pinching inside Logseq Shape issue
           e.stopPropagation()
         }
       },
       [tlEventsEnabled]
     )
 
-    const commitChange = React.useCallback((id: string) => {
+    // It is a bit weird to update shapes here. Is there a better place?
+    React.useEffect(() => {
+      if (this.props.collapsed && isActivated) {
+        // Should temporarily disable collapsing
+        this.update({
+          size: [this.props.size[0], this.props.collapsedHeight],
+        })
+        return () => {
+          this.update({
+            size: [this.props.size[0], HEADER_HEIGHT],
+          })
+        }
+      }
+      return () => {
+        // no-ops
+      }
+    }, [isActivated])
+
+    const onPageNameChanged = React.useCallback((id: string) => {
       transaction(() => {
         this.update({
           pageId: id,
@@ -141,13 +213,15 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
       })
     }, [])
 
+    if (!Page) {
+      return null // not being correctly configured
+    }
+
     return (
       <HTMLContainer
         style={{
-          overflow: 'hidden',
           pointerEvents: 'all',
           opacity: isErasing ? 0.2 : opacity,
-          backgroundColor: 'var(--ls-primary-background-color)',
         }}
         {...events}
       >
@@ -156,30 +230,53 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
           onPointerDown={stop}
           onPointerUp={stop}
           style={{
+            width: '100%',
+            height: '100%',
             pointerEvents: isActivated ? 'all' : 'none',
           }}
         >
           {this.draft ? (
-            <LogseqQuickSearch onChange={commitChange} />
+            <LogseqQuickSearch onChange={onPageNameChanged} />
           ) : (
             <div
+              className="tl-logseq-portal-container"
               style={{
-                width: '100%',
-                overflow: 'auto',
-                overscrollBehavior: 'none',
-                height: pageId ? 'calc(100% - 33px)' : '100%',
-                userSelect: 'none',
-                boxShadow: isActivated
-                  ? '0px 0px 0 var(--tl-binding-distance) var(--tl-binding)'
-                  : '',
-                opacity: isSelected ? 0.5 : 1,
+                background: fill,
+                boxShadow:
+                  isActivated || isBinding
+                    ? '0px 0px 0 var(--tl-binding-distance) var(--tl-binding)'
+                    : 'var(--shadow-medium)',
+                opacity: isSelected ? 0.8 : 1,
+                color: stroke,
+                // @ts-expect-error ???
+                '--ls-primary-background-color': fill,
+                '--ls-primary-text-color': stroke,
+                '--ls-title-text-color': stroke,
               }}
             >
-              {pageId && Page ? (
-                <div style={{ padding: '12px', height: '100%', cursor: 'default' }}>
-                  <Page pageId={pageId} />
+              <LogseqPortalShapeHeader type="P" pageId={pageId} />
+              {(!this.props.collapsed || isActivated) && (
+                <div
+                  style={{
+                    width: '100%',
+                    overflow: 'auto',
+                    borderRadius: '8px',
+                    overscrollBehavior: 'none',
+                    height: '100%',
+                    flex: 1,
+                  }}
+                >
+                  <div
+                    style={{
+                      padding: '12px',
+                      height: '100%',
+                      cursor: 'default',
+                    }}
+                  >
+                    <Page pageId={pageId} />
+                  </div>
                 </div>
-              ) : null}
+              )}
             </div>
           )}
         </div>
@@ -188,18 +285,14 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   })
 
   ReactIndicator = observer(() => {
-    const {
-      props: {
-        size: [w, h],
-      },
-    } = this
-    return <rect width={w} height={h} fill="transparent" />
+    const bounds = this.getBounds()
+    return <rect width={bounds.width} height={bounds.height} fill="transparent" />
   })
 
   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[0] = Math.max(props.size[0], 240)
+      props.size[1] = Math.max(props.size[1], HEADER_HEIGHT)
     }
     return withClampedStyles(props)
   }

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/PencilShape.tsx

@@ -25,7 +25,7 @@ export class PencilShape extends TLDrawShape<PencilShapeProps> {
     point: [0, 0],
     points: [],
     isComplete: false,
-    stroke: '#000000',
+    stroke: 'var(--tl-foreground)',
     fill: '#ffffff',
     strokeWidth: 2,
     opacity: 1,

+ 1 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx

@@ -4,7 +4,6 @@ import { HTMLContainer, TLComponentProps, TLTextMeasure } from '@tldraw/react'
 import { TextUtils, TLBounds, TLResizeStartInfo, TLTextShape, TLTextShapeProps } from '@tldraw/core'
 import { observer } from 'mobx-react-lite'
 import { CustomStyleProps, withClampedStyles } from './style-props'
-import { NumberInput } from '~components/inputs/NumberInput'
 
 export interface TextShapeProps extends TLTextShapeProps, CustomStyleProps {
   borderRadius: number
@@ -33,7 +32,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     padding: 4,
     fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
     borderRadius: 0,
-    stroke: '#000000',
+    stroke: 'var(--tl-foreground)',
     fill: '#ffffff',
     strokeWidth: 2,
     opacity: 1,

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/arrow/Arrow.tsx

@@ -32,7 +32,7 @@ export const Arrow = React.memo(function StraightArrow({
   // Path between start and end points
   const path = 'M' + Vec.toFixed(start) + 'L' + Vec.toFixed(end)
   // Arrowheads
-  const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
+  const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 16)
   const startArrowHead = decorationStart
     ? getStraightArrowHeadPoints(start, end, arrowHeadLength)
     : null

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/arrow/arrowHelpers.ts

@@ -46,7 +46,7 @@ export function getArrowPath(
 ) {
   const strokeWidth = style.strokeWidth
   const arrowDist = Vec.dist(start, end)
-  const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
+  const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 16)
   const path: (string | number)[] = []
   path.push(`M ${start} L ${end}`)
   if (decorationStart) {

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

@@ -2,8 +2,10 @@ 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']
+  // not sure why "c" is not working in Logseq?
+  static shortcut = ['c', 'x']
   Shape = LineShape
 }

+ 193 - 37
tldraw/apps/tldraw-logseq/src/styles.css

@@ -1,15 +1,18 @@
 /* TODO: move to useStylesheet */
 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap');
 
-:root {
-  --color-panel: #ffffff;
-  --color-text: #000000;
-  --color-hover: #00000011;
+.logseq-tldraw {
+  --color-panel: var(--ls-tertiary-background-color);
+  --color-panel-inverted: var(--ls-secondary-text-color);
+  --color-text: var(--ls-primary-text-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,17 +23,19 @@
   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;
   padding: 4px 8px;
+  opacity: 1;
 }
 
 .logseq-tldraw .toolbar {
@@ -81,13 +86,14 @@
 .logseq-tldraw .contextbar {
   pointer-events: all;
   position: relative;
-  background-color: var(--color-panel);
+  background-color: #fff;
+  color: #a4b5b6;
   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);
@@ -125,23 +131,57 @@
   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: #8ec2c2;
+}
+
+.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 {
   height: 24px;
-  padding: 4px;
+  padding: 0;
   background: none;
-  border: 1px solid black;
   border-radius: 2px;
 }
 
-.logseq-tldraw .input > label {
-  font-size: 10px;
+.logseq-tldraw .text-input:focus {
+  outline: none;
 }
 
 .logseq-tldraw .primary-tools {
@@ -178,8 +218,8 @@
   flex-flow: column;
   border-radius: 8px;
   overflow: hidden;
-  padding: 4px;
-  gap: 4px;
+  padding: 8px;
+  gap: 8px;
 }
 
 .logseq-tldraw .floating-panel > button {
@@ -188,16 +228,17 @@
 
 .logseq-tldraw .primary-tools .button {
   position: relative;
-  height: 40px;
-  width: 40px;
+  height: 36px;
+  width: 36px;
   display: flex;
   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;
+  color: var(--ls-secondary-text-color);
 }
 
 .logseq-tldraw .primary-tools .button:hover {
@@ -226,21 +267,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;
@@ -321,8 +359,8 @@
 
 .logseq-tldraw .text-label-wrapper {
   position: absolute;
-  top: 0px;
-  left: 0px;
+  top: 0;
+  left: 0;
   width: 100%;
   height: 100%;
   display: flex;
@@ -339,7 +377,7 @@
   min-height: 1px;
   min-width: 1px;
   line-height: 1;
-  outline: 0px;
+  outline: 0;
   font-weight: 500;
   text-align: center;
   backface-visibility: hidden;
@@ -350,8 +388,8 @@
 
 .logseq-tldraw .text-label-textarea {
   position: absolute;
-  top: 0px;
-  left: 0px;
+  top: 0;
+  left: 0;
   z-index: 1;
   width: 100%;
   height: 100%;
@@ -363,7 +401,7 @@
   min-width: inherit;
   line-height: inherit;
   letter-spacing: inherit;
-  outline: 0px;
+  outline: 0;
   font-weight: inherit;
   overflow: hidden;
   backface-visibility: hidden;
@@ -412,3 +450,121 @@
 .logseq-tldraw .preview-minimap-toggle[data-active='true'] {
   background: #eee;
 }
+
+.logseq-tldraw .tl-quick-search {
+  width: fit-content;
+  position: relative;
+}
+
+.logseq-tldraw .tl-quick-search-icon {
+  flex-shrink: 0;
+  margin-right: 12px;
+}
+
+.logseq-tldraw .tl-quick-search-input-container {
+  display: flex;
+  align-items: center;
+  font-size: 16px;
+  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 {
+  display: inline-grid;
+  vertical-align: top;
+  align-items: center;
+  position: relative;
+  margin: 5px;
+}
+
+.logseq-tldraw .tl-quick-search-input {
+  grid-area: 1/2;
+  width: auto;
+  line-height: 1;
+}
+
+.logseq-tldraw .tl-quick-search-input-sizer::after {
+  content: attr(data-value) ' ';
+  visibility: hidden;
+  white-space: pre-wrap;
+  grid-area: 1/2;
+  width: auto;
+}
+
+.logseq-tldraw .tl-quick-search-options {
+  position: absolute;
+  top: calc(100% + 8px);
+  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;
+  overscroll-behavior: none;
+}
+
+.logseq-tldraw .tl-quick-search-option {
+  padding: 8px 16px;
+  cursor: pointer;
+  display: flex;
+  font-size: 0.875rem;
+  line-height: 1.25rem;
+}
+
+.logseq-tldraw .tl-quick-search-option:hover {
+  background-color: var(--ls-menu-hover-color, #f4f5f7);
+}
+
+.logseq-tldraw .tl-logseq-portal-container {
+  width: 100%;
+  overflow: auto;
+  border-radius: 8px;
+  overscroll-behavior: none;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background: var(--ls-secondary-background-color);
+  opacity: 1;
+}
+
+.logseq-tldraw .tl-logseq-portal-header {
+  height: 40px;
+  width: 100%;
+  flex-shrink: 0;
+  background: transparent;
+  display: flex;
+  color: var(--ls-title-text-color);
+  padding: 0 1rem;
+  align-items: center;
+  gap: 0.5em;
+}
+
+.logseq-tldraw .tl-logseq-portal-header .type-tag {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  font-size: 12px;
+  line-height: 1;
+  padding: 1px 4px;
+  color: #fff;
+  background: rgba(0, 0, 0, 0.5);
+  border: 1px solid #fff;
+  border-radius: 4px;
+  order: 0;
+  flex-grow: 0;
+  transform: translateY(-1px);
+}
+
+html[data-theme='light'] .logseq-tldraw .tl-logseq-portal-header {
+  backdrop-filter: brightness(0.9);
+}
+
+html[data-theme='dark'] .logseq-tldraw .tl-logseq-portal-header {
+  backdrop-filter: brightness(1.2);
+}

+ 3 - 0
tldraw/demo/postcss.config.js

@@ -1,5 +1,8 @@
 module.exports = {
   plugins: {
+    'postcss-import': {},
+    'postcss-nested': {},
+    'postcss-import-ext-glob': {},
     tailwindcss: {},
     autoprefixer: {},
   },

+ 46 - 1
tldraw/demo/src/App.jsx

@@ -1,4 +1,5 @@
 import React from 'react'
+import ReactDOM from 'react-dom'
 import { App as TldrawApp } from 'tldraw-logseq'
 
 const storingKey = 'playground.index'
@@ -53,9 +54,53 @@ const Page = props => {
   )
 }
 
+const ThemeSwitcher = ({ theme, setTheme }) => {
+  const [anchor, setAnchor] = React.useState(null)
+  React.useEffect(() => {
+    if (anchor) {
+      return
+    }
+    let el = document.querySelector('#theme-switcher')
+    if (!el) {
+      el = document.createElement('div')
+      el.id = 'theme-switcher'
+      let timer = setInterval(() => {
+        const statusBarAnchor = document.querySelector('#tl-statusbar-anchor')
+        if (statusBarAnchor) {
+          statusBarAnchor.appendChild(el)
+          setAnchor(el)
+          clearInterval(timer)
+        }
+      }, 50)
+    }
+  })
+
+  React.useEffect(() => {
+    document.documentElement.setAttribute('data-theme', theme)
+  }, [theme])
+
+  if (!anchor) {
+    return null
+  }
+
+  return ReactDOM.createPortal(
+    <button
+      className="flex items-center justify-center mx-2 bg-grey"
+      style={{ fontSize: '1em' }}
+      onClick={() => setTheme(t => (t === 'dark' ? 'light' : 'dark'))}
+    >
+      {theme} theme
+    </button>,
+    anchor
+  )
+}
+
 export default function App() {
+  const [theme, setTheme] = React.useState('light')
+
   return (
-    <div className="h-screen w-screen">
+    <div className={`h-screen w-screen`}>
+      <ThemeSwitcher theme={theme} setTheme={setTheme} />
       <TldrawApp
         PageComponent={Page}
         searchHandler={q => (q ? list : [])}

+ 1 - 2
tldraw/demo/src/main.jsx

@@ -1,7 +1,6 @@
+import '../../../tailwind.all.css'
 import React from 'react'
 import ReactDOM from 'react-dom'
-import 'tldraw-logseq/styles.css'
-import '../../../public/static/css/common.css'
 
 import App from './App'
 

+ 2 - 4
tldraw/demo/tailwind.config.js

@@ -1,8 +1,6 @@
 module.exports = {
-  content: [
-    "./index.html",
-    "./src/**/*.{vue,js,ts,jsx,tsx}",
-  ],
+  // just import everything for ease of dev 
+  safelist: [{ pattern: /.*/ }],
   theme: {
     extend: {},
   },

+ 33 - 2
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -2,7 +2,7 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import { Vec } from '@tldraw/vec'
-import { action, computed, makeObservable, observable } from 'mobx'
+import { action, computed, makeObservable, observable, toJS } from 'mobx'
 import { BoundsUtils, KeyUtils } from '~utils'
 import {
   TLSelectTool,
@@ -217,6 +217,7 @@ export class TLApp<
       currentPageId: this.currentPageId,
       selectedIds: Array.from(this.selectedIds.values()),
       pages: Array.from(this.pages.values()).map(page => page.serialized),
+      assets: Object.values(this.assets),
     }
   }
 
@@ -345,6 +346,28 @@ export class TLApp<
     return this
   }
 
+  copy = () => {
+    if (this.selectedShapesArray.length > 0) {
+      const tldrawString = JSON.stringify({
+        type: 'logseq/whiteboard-shapes',
+        shapes: this.selectedShapesArray.map(shape => shape.serialized),
+      })
+      navigator.clipboard.write([
+        new ClipboardItem({
+          'text/plain': new Blob([tldrawString], { type: 'text/plain' }),
+        }),
+      ])
+    }
+  }
+
+  paste = (e?: ClipboardEvent) => {
+    this.notify('paste', {
+      point: this.inputs.currentPoint,
+    })
+    // This callback may be over-written manually, see useSetup.ts in React.
+    return void null
+  }
+
   dropFiles = (files: FileList, point?: number[]) => {
     this.notify('drop-files', {
       files: Array.from(files),
@@ -447,6 +470,12 @@ export class TLApp<
     } else {
       this.selectionRotation = 0
     }
+    if (process.env.NODE_ENV === 'development') {
+      console.log(
+        'setSelectedShapes',
+        newSelectedShapes.map(s => toJS(s.serialized))
+      )
+    }
     return this
   }
 
@@ -598,6 +627,7 @@ export class TLApp<
     const { selectedShapesArray } = this
     return (
       this.isIn('select') &&
+      !this.isInAny('select.translating', 'select.pinching') &&
       ((selectedShapesArray.length === 1 && !selectedShapesArray[0]?.hideSelection) ||
         selectedShapesArray.length > 1)
     )
@@ -606,6 +636,7 @@ export class TLApp<
   @computed get showSelectionDetail() {
     return (
       this.isIn('select') &&
+      !this.isInAny('select.translating', 'select.pinching') &&
       this.selectedShapes.size > 0 &&
       !this.selectedShapesArray.every(shape => shape.hideSelectionDetail)
     )
@@ -655,7 +686,7 @@ export class TLApp<
         'select.pointingRotateHandle',
         'select.pointingResizeHandle'
       ) &&
-      selectedShapesArray.length > 0 &&
+      selectedShapesArray.length === 1 &&
       !selectedShapesArray.every(shape => shape.hideResizeHandles)
     )
   }

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

@@ -29,6 +29,7 @@ export interface TLShapeProps {
   parentId: string
   name?: string
   point: number[]
+  size?: number[]
   scale?: number[]
   rotation?: number
   handles?: Record<string, TLHandle>
@@ -97,7 +98,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
@@ -108,7 +109,7 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
   bindingDistance = BINDING_DISTANCE
 
   // For smart shape
-  @observable draft = false
+  @observable private _draft = false
   @observable private isDirty = false
   @observable private lastSerialized: TLShapeModel<P> | undefined
 
@@ -118,8 +119,13 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
     return this.props.id
   }
 
+  @computed
+  get draft() {
+    return this._draft
+  }
+
   @action setDraft(draft: boolean) {
-    this.draft = draft
+    this._draft = draft
   }
 
   @action setIsDirty(isDirty: boolean) {

+ 3 - 1
tldraw/packages/core/src/lib/tools/TLDotTool/states/CreatingState.tsx

@@ -20,13 +20,15 @@ export class CreatingState<
 
   onEnter = () => {
     const { Shape } = this.tool
-    this.offset = [Shape.defaultProps.size[0] / 2, Shape.defaultProps.size[1] / 2]
     const shape = new Shape({
       id: uniqueId(),
       parentId: this.app.currentPage.id,
       point: Vec.sub(this.app.inputs.originPoint, this.offset),
       size: Shape.defaultProps.size,
     } as any)
+    if (Shape.smart) {
+      shape.setDraft(true)
+    }
     this.creatingShape = shape
   }
 

+ 1 - 0
tldraw/packages/core/src/lib/tools/TLSelectTool/states/BrushingState.ts

@@ -48,6 +48,7 @@ export class BrushingState<
           ? BoundsUtils.boundsContain(brushBounds, shape.rotatedBounds)
           : shape.hitTestBounds(brushBounds)
       )
+      .filter(s => !s.draft)
 
     if (shiftKey) {
       if (hits.every(hit => this.initialSelectedShapes.includes(hit))) {

+ 4 - 1
tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts

@@ -83,8 +83,11 @@ export class IdleState<
           const { selectionBounds, inputs } = this.app
           if (selectionBounds && PointUtils.pointInBounds(inputs.currentPoint, selectionBounds)) {
             this.tool.transition('pointingShapeBehindBounds', info)
-          } else {
+          } else if (!info.shape.draft) {
             this.tool.transition('pointingShape', info)
+          } else {
+            // as if clicking the canvas
+            this.tool.transition('pointingCanvas')
           }
         }
         break

+ 1 - 0
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingCanvasState.ts

@@ -52,6 +52,7 @@ export class PointingCanvasState<
           parentId: this.app.currentPage.id,
           point: [...this.app.inputs.originPoint],
         })
+        shape.setDraft(true)
         this.app.setActivatedShapes([shape.id])
         this.app.currentPage.addShapes(shape)
       }

+ 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) {

+ 4 - 0
tldraw/packages/core/src/types/types.ts

@@ -156,6 +156,10 @@ export type TLSubscriptionEvent =
       event: 'drop-files'
       info: { files: File[]; point: number[] }
     }
+  | {
+      event: 'paste'
+      info: { point: number[] }
+    }
   | {
       event: 'create-assets'
       info: { assets: TLAsset[] }

+ 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}

+ 0 - 20
tldraw/packages/react/src/hooks/useCanvasEvents.ts

@@ -74,23 +74,3 @@ export function useCanvasEvents() {
 
   return events
 }
-
-function fileToBase64(file: Blob): Promise<string | ArrayBuffer | null> {
-  return new Promise((resolve, reject) => {
-    if (file) {
-      const reader = new FileReader()
-      reader.readAsDataURL(file)
-      reader.onload = () => resolve(reader.result)
-      reader.onerror = error => reject(error)
-      reader.onabort = error => reject(error)
-    }
-  })
-}
-
-function getSizeFromSrc(dataURL: string): Promise<number[]> {
-  return new Promise(resolve => {
-    const img = new Image()
-    img.onload = () => resolve([img.width, img.height])
-    img.src = dataURL
-  })
-}

+ 10 - 1
tldraw/packages/react/src/hooks/useKeyboardEvents.ts

@@ -1,9 +1,10 @@
 import * as React from 'react'
-import { useRendererContext } from '~hooks'
+import { useApp, useRendererContext } from '~hooks'
 import { TLTargetType } from '@tldraw/core'
 import type { TLReactCustomEvents } from '~types'
 
 export function useKeyboardEvents() {
+  const app = useApp()
   const { callbacks } = useRendererContext()
 
   React.useEffect(() => {
@@ -15,6 +16,14 @@ export function useKeyboardEvents() {
     }
     window.addEventListener('keydown', onKeyDown)
     window.addEventListener('keyup', onKeyUp)
+    document.addEventListener('paste', (e) => {
+      e.preventDefault()
+      app.paste(e)
+    })
+    document.addEventListener('copy', (e) => {
+      e.preventDefault()
+      app.copy()
+    })
     return () => {
       window.removeEventListener('keydown', onKeyDown)
       window.removeEventListener('keyup', onKeyUp)

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

@@ -19,6 +19,7 @@ export function useSetup<
     onDeleteAssets,
     onDeleteShapes,
     onFileDrop,
+    onPaste
   } = props
 
   React.useLayoutEffect(() => {
@@ -44,6 +45,7 @@ export function useSetup<
     if (onDeleteShapes) unsubs.push(app.subscribe('delete-shapes', onDeleteShapes))
     if (onDeleteAssets) unsubs.push(app.subscribe('delete-assets', onDeleteAssets))
     if (onFileDrop) unsubs.push(app.subscribe('drop-files', onFileDrop))
+    if (onPaste) unsubs.push(app.subscribe('paste', onPaste))
     // Kind of unusual, is this the right pattern?
 
     return () => unsubs.forEach(unsub => unsub())

+ 7 - 4
tldraw/packages/react/src/hooks/useStylesheet.ts

@@ -15,7 +15,7 @@ function makeCssTheme<T = AnyTheme>(prefix: string, theme: T) {
   }, '')
 }
 
-function useTheme<T = AnyTheme>(prefix: string, theme: T, selector = ':root') {
+function useTheme<T = AnyTheme>(prefix: string, theme: T, selector = '.logseq-tldraw') {
   React.useLayoutEffect(() => {
     const style = document.createElement('style')
     const cssTheme = makeCssTheme(prefix, theme)
@@ -72,9 +72,9 @@ const defaultTheme: TLTheme = {
   selectStroke: 'rgb(66, 133, 244)',
   selectFill: 'rgba(65, 132, 244, 0.05)',
   binding: 'rgba(65, 132, 244, 0.5)',
-  background: 'rgb(248, 249, 250)',
-  foreground: 'rgb(51, 51, 51)',
-  grid: 'rgba(144, 144, 144, .9)',
+  background: 'var(--ls-primary-background-color)',
+  foreground: 'var(--ls-secondary-text-color)',
+  grid: 'var(--ls-quaternary-background-color)',
 }
 
 const tlcss = css`
@@ -151,6 +151,8 @@ const tlcss = css`
     background-color: var(--tl-background);
     cursor: var(--tl-cursor) !important;
     box-sizing: border-box;
+    color: var(--tl-foreground);
+    will-change: transform;
   }
 
   .tl-overlay {
@@ -207,6 +209,7 @@ const tlcss = css`
     left: 0px;
     transform-origin: center center;
     contain: layout style size;
+    will-change: transform;
   }
 
   .tl-positioned {

+ 1 - 0
tldraw/packages/react/src/types/TLReactSubscriptions.tsx

@@ -39,4 +39,5 @@ export interface TLReactCallbacks<
   onDeleteShapes: TLReactCallback<S, R, 'delete-shapes'>
   onDeleteAssets: TLReactCallback<S, R, 'delete-assets'>
   onFileDrop: TLReactCallback<S, R, 'drop-files'>
+  onPaste: TLReactCallback<S, R, 'paste'>
 }

+ 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"