Browse Source

Merge pull request #7336 from logseq/fix/wb-fixes

fix(whiteboard): whiteboard ux issues
Gabriel Horner 2 years ago
parent
commit
962998a7b2
30 changed files with 231 additions and 170 deletions
  1. 5 2
      src/main/electron/listener.cljs
  2. 2 2
      src/main/frontend/components/block.cljs
  3. 3 6
      src/main/frontend/components/content.cljs
  4. 3 3
      src/main/frontend/components/page.cljs
  5. 2 1
      src/main/frontend/components/page_menu.cljs
  6. 4 2
      src/main/frontend/components/sidebar.cljs
  7. 1 0
      src/main/frontend/components/sidebar.css
  8. 25 18
      src/main/frontend/components/whiteboard.cljs
  9. 15 7
      src/main/frontend/db/model.cljs
  10. 1 1
      src/main/frontend/extensions/tldraw.cljs
  11. 3 2
      src/main/frontend/handler/page.cljs
  12. 1 1
      src/main/frontend/mobile/index.css
  13. 3 2
      src/main/frontend/modules/file/core.cljs
  14. 7 7
      src/main/frontend/ui.cljs
  15. 10 0
      tldraw/apps/tldraw-logseq/src/components/Devtools/Devtools.tsx
  16. 2 24
      tldraw/apps/tldraw-logseq/src/components/StatusBar/StatusBar.tsx
  17. 5 3
      tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts
  18. 8 8
      tldraw/apps/tldraw-logseq/src/lib/preview-manager.tsx
  19. 8 24
      tldraw/apps/tldraw-logseq/src/lib/shapes/ImageShape.tsx
  20. 23 34
      tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx
  21. 2 1
      tldraw/apps/tldraw-logseq/src/styles.css
  22. 3 0
      tldraw/demo/postcss.config.js
  23. 68 15
      tldraw/demo/src/App.jsx
  24. 1 0
      tldraw/packages/core/src/lib/TLSettings.ts
  25. 1 1
      tldraw/packages/core/src/utils/ColorUtils.ts
  26. 1 0
      tldraw/packages/react/package.json
  27. 12 4
      tldraw/packages/react/src/components/ui/Grid/Grid.tsx
  28. 4 1
      tldraw/packages/react/src/hooks/useGestureEvents.ts
  29. 7 0
      tldraw/packages/react/src/hooks/useStylesheet.ts
  30. 1 1
      tldraw/yarn.lock

+ 5 - 2
src/main/electron/listener.cljs

@@ -92,10 +92,13 @@
                        (let [{:keys [page-name block-id file]} (bean/->clj data)]
                        (let [{:keys [page-name block-id file]} (bean/->clj data)]
                          (cond
                          (cond
                            page-name
                            page-name
-                           (let [db-page-name (db-model/get-redirect-page-name page-name)]
+                           (let [db-page-name (db-model/get-redirect-page-name page-name)
+                                 whiteboard? (db-model/whiteboard-page? db-page-name)]
                              ;; No error handling required, as a page name is always valid
                              ;; No error handling required, as a page name is always valid
                              ;; Open new page if the page does not exist
                              ;; Open new page if the page does not exist
-                             (editor-handler/insert-first-page-block-if-not-exists! db-page-name))
+                             (if whiteboard?
+                               (route-handler/redirect-to-whiteboard! page-name {:block-id block-id})
+                               (editor-handler/insert-first-page-block-if-not-exists! db-page-name)))
 
 
                            block-id
                            block-id
                            (if (db-model/get-block-by-uuid block-id)
                            (if (db-model/get-block-by-uuid block-id)

+ 2 - 2
src/main/frontend/components/block.cljs

@@ -2534,7 +2534,7 @@
                             (filterv identity)
                             (filterv identity)
                             (map (fn [x] (if (vector? x)
                             (map (fn [x] (if (vector? x)
                                            (let [[block label] x]
                                            (let [[block label] x]
-                                             (breadcrumb-fragment config block label opts))
+                                             (rum/with-key (breadcrumb-fragment config block label opts) (:block/uuid block)))
                                            [:span.opacity-70 "⋯"])))
                                            [:span.opacity-70 "⋯"])))
                             (interpose (breadcrumb-separator)))]
                             (interpose (breadcrumb-separator)))]
         [:div.breadcrumb.block-parents.flex-row.flex-1
         [:div.breadcrumb.block-parents.flex-row.flex-1
@@ -3603,7 +3603,7 @@
             (when (seq blocks)
             (when (seq blocks)
               (let [alias? (:block/alias? page)
               (let [alias? (:block/alias? page)
                     page (db/entity (:db/id page))
                     page (db/entity (:db/id page))
-                    whiteboard? (= "whiteboard" (:block/type page))]
+                    whiteboard? (model/whiteboard-page? page)]
                 [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
                 [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
                              (:ref? config)
                              (:ref? config)
                              (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))
                              (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))

+ 3 - 6
src/main/frontend/components/content.cljs

@@ -362,12 +362,9 @@
     (let [page-menu-options (page-menu/page-menu page)]
     (let [page-menu-options (page-menu/page-menu page)]
       [:.menu-links-wrapper
       [:.menu-links-wrapper
        (for [{:keys [title options]} page-menu-options]
        (for [{:keys [title options]} page-menu-options]
-         (ui/menu-link
-          (merge
-           {:key title}
-           options)
-          title
-          nil))])))
+         (rum/with-key
+           (ui/menu-link options title nil)
+           title))])))
 
 
 ;; TODO: content could be changed
 ;; TODO: content could be changed
 ;; Also, keyboard bindings should only be activated after
 ;; Also, keyboard bindings should only be activated after

+ 3 - 3
src/main/frontend/components/page.cljs

@@ -370,8 +370,8 @@
           journal? (db/journal-page? page-name)
           journal? (db/journal-page? page-name)
           fmt-journal? (boolean (date/journal-title->int page-name))
           fmt-journal? (boolean (date/journal-title->int page-name))
           sidebar? (:sidebar? option)
           sidebar? (:sidebar? option)
-          whiteboard? (:whiteboard? option)
-          whiteboard-page? (model/whiteboard-page? page-name)
+          whiteboard? (:whiteboard? option) ;; in a whiteboard portal shape?
+          whiteboard-page? (model/whiteboard-page? page-name) ;; is this page a whiteboard?
           route-page-name path-page-name
           route-page-name path-page-name
           page (if block?
           page (if block?
                  (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
                  (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
@@ -401,7 +401,7 @@
               {:key path-page-name
               {:key path-page-name
                :class (util/classnames [{:is-journals (or journal? fmt-journal?)}])})
                :class (util/classnames [{:is-journals (or journal? fmt-journal?)}])})
 
 
-       (if whiteboard-page?
+       (if (and whiteboard-page? (not sidebar?))
          [:div ((state/get-component :whiteboard/tldraw-preview) page-name)] ;; FIXME: this is not reactive
          [:div ((state/get-component :whiteboard/tldraw-preview) page-name)] ;; FIXME: this is not reactive
          [:div.relative
          [:div.relative
           (when (and (not sidebar?) (not block?))
           (when (and (not sidebar?) (not block?))

+ 2 - 1
src/main/frontend/components/page_menu.cljs

@@ -113,7 +113,8 @@
             {:title   (t :page/delete)
             {:title   (t :page/delete)
              :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
              :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
 
 
-          (when-not (mobile-util/native-platform?)
+          (when (and (not (mobile-util/native-platform?)) 
+                     (state/get-current-page))
             {:title (t :page/presentation-mode)
             {:title (t :page/presentation-mode)
              :options {:on-click (fn []
              :options {:on-click (fn []
                                    (state/sidebar-add-block!
                                    (state/sidebar-add-block!

+ 4 - 2
src/main/frontend/components/sidebar.cljs

@@ -2,6 +2,7 @@
   (:require [cljs-drag-n-drop.core :as dnd]
   (:require [cljs-drag-n-drop.core :as dnd]
             [clojure.string :as string]
             [clojure.string :as string]
             [frontend.components.command-palette :as command-palette]
             [frontend.components.command-palette :as command-palette]
+            [frontend.components.find-in-page :as find-in-page]
             [frontend.components.header :as header]
             [frontend.components.header :as header]
             [frontend.components.journal :as journal]
             [frontend.components.journal :as journal]
             [frontend.components.onboarding :as onboarding]
             [frontend.components.onboarding :as onboarding]
@@ -12,7 +13,6 @@
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
             [frontend.components.widgets :as widgets]
-            [frontend.components.find-in-page :as find-in-page]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db :as db]
@@ -392,7 +392,9 @@
                             (when-let [id (state/get-edit-input-id)]
                             (when-let [id (state/get-edit-input-id)]
                               (let [format (:block/format (state/get-edit-block))]
                               (let [format (:block/format (state/get-edit-block))]
                                 (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
                                 (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
-                  (common-handler/listen-to-scroll! element))
+                  (common-handler/listen-to-scroll! element)
+                  (when (:margin-less-pages? (first (:rum/args state))) ;; makes sure full screen pages displaying without scrollbar
+                    (set! (.. element -scrollTop) 0)))
                 state)}
                 state)}
   [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
   [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
   (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
   (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)

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

@@ -72,6 +72,7 @@
 #main-content-container[data-is-margin-less-pages=true] {
 #main-content-container[data-is-margin-less-pages=true] {
   padding: 0 !important;
   padding: 0 !important;
   position: relative;
   position: relative;
+  overflow: hidden;
 }
 }
 
 
 .left-sidebar-inner {
 .left-sidebar-inner {

+ 25 - 18
src/main/frontend/components/whiteboard.cljs

@@ -74,7 +74,23 @@
                 :min-height "40px"
                 :min-height "40px"
                 :max-height "420px"
                 :max-height "420px"
                 :left offset-x
                 :left offset-x
-                :top offset-y}} children])]))
+                :top offset-y}}
+       (when d-open children)])]))
+
+(rum/defc dropdown-menu
+  [{:keys [label children classname hover?]}]
+  (let [[open-flag set-open-flag] (rum/use-state 0)
+        open? (> open-flag (if hover? 0 1))
+        d-open-flag (rum/use-memo #(util/debounce 200 set-open-flag) [])]
+    (dropdown
+     [:div {:class (str classname (when open? " open"))
+            :on-mouse-enter (fn [] (d-open-flag #(if (= % 0) 1 %)))
+            :on-mouse-leave (fn [] (d-open-flag #(if (= % 2) % 0)))
+            :on-click (fn [e]
+                        (util/stop e)
+                        (d-open-flag (fn [o] (if (not= o 2) 2 0))))}
+      (if (fn? label) (label open?) label)]
+     children open? #(set-open-flag 0))))
 
 
 (rum/defc page-refs-count < rum/static
 (rum/defc page-refs-count < rum/static
   ([page-name classname]
   ([page-name classname]
@@ -82,24 +98,15 @@
   ([page-name classname render-fn]
   ([page-name classname render-fn]
    (let [page-entity (model/get-page page-name)
    (let [page-entity (model/get-page page-name)
          block-uuid (:block/uuid page-entity)
          block-uuid (:block/uuid page-entity)
-         refs-count (count (:block/_refs page-entity))
-         [open-flag set-open-flag] (rum/use-state 0)
-         open? (not= open-flag 0)
-         d-open-flag (rum/use-memo #(util/debounce 200 set-open-flag) [])]
+         refs-count (count (:block/_refs page-entity))]
      (when (> refs-count 0)
      (when (> refs-count 0)
-       (dropdown
-        [:div.flex.items-center.gap-2.whiteboard-page-refs-count
-         {:class (str classname (when open? " open"))
-          :on-mouse-enter (fn [] (d-open-flag #(if (= % 0) 1 %)))
-          :on-mouse-leave (fn [] (d-open-flag #(if (= % 2) % 0)))
-          :on-click (fn [e]
-                      (util/stop e)
-                      (d-open-flag (fn [o] (if (not= o 2) 2 0))))}
-         [:div.open-page-ref-link refs-count]
-         (when render-fn (render-fn open? refs-count))]
-        (reference/block-linked-references block-uuid)
-        open?
-        #(set-open-flag 0))))))
+       (dropdown-menu {:classname classname
+                       :label (fn [open?]
+                                [:div.flex.items-center.gap-2
+                                 [:div.open-page-ref-link refs-count]
+                                 (when render-fn (render-fn open? refs-count))])
+                       :hover? true
+                       :children (reference/block-linked-references block-uuid)})))))
 
 
 (defn- get-page-display-name
 (defn- get-page-display-name
   [page-name]
   [page-name]

+ 15 - 7
src/main/frontend/db/model.cljs

@@ -1677,13 +1677,21 @@
    macro-name))
    macro-name))
 
 
 (defn whiteboard-page?
 (defn whiteboard-page?
-  [page-name]
-  (let [page (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page-name)])]
-    (or
-     (= "whiteboard" (:block/type page))
-     (when-let [file (:block/file page)]
-       (when-let [path (:file/path (db-utils/entity (:db/id file)))]
-         (gp-config/whiteboard? path))))))
+  "Given a page name or a page object, check if it is a whiteboard page"
+  [page]
+  (cond
+    (string? page)
+    (let [page (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page)])]
+      (or
+       (= "whiteboard" (:block/type page))
+       (when-let [file (:block/file page)]
+         (when-let [path (:file/path (db-utils/entity (:db/id file)))]
+           (gp-config/whiteboard? path)))))
+
+    (seq page)
+    (= "whiteboard" (:block/type page))
+
+    :else false))
 
 
 (defn get-all-whiteboards
 (defn get-all-whiteboards
   [repo]
   [repo]

+ 1 - 1
src/main/frontend/extensions/tldraw.cljs

@@ -18,7 +18,7 @@
 
 
 (def tldraw (r/adapt-class (gobj/get TldrawLogseq "App")))
 (def tldraw (r/adapt-class (gobj/get TldrawLogseq "App")))
 
 
-(def generate-preview (gobj/get TldrawLogseq "generateJSXFromApp"))
+(def generate-preview (gobj/get TldrawLogseq "generateJSXFromModel"))
 
 
 (rum/defc page-cp
 (rum/defc page-cp
   [props]
   [props]

+ 3 - 2
src/main/frontend/handler/page.cljs

@@ -61,7 +61,8 @@
     (gp-util/safe-subs s 0 200)))
     (gp-util/safe-subs s 0 200)))
 
 
 (defn get-page-file-path
 (defn get-page-file-path
-  ([] (get-page-file-path (state/get-current-page)))
+  ([] (get-page-file-path (or (state/get-current-page)
+                              (state/get-current-whiteboard))))
   ([page-name]
   ([page-name]
    (when page-name
    (when page-name
      (let [page-name (util/page-name-sanity-lc page-name)]
      (let [page-name (util/page-name-sanity-lc page-name)]
@@ -475,7 +476,7 @@
 
 
       ;; Redirect to the newly renamed page
       ;; Redirect to the newly renamed page
       (when redirect?
       (when redirect?
-        (route-handler/redirect! {:to          (if (= "whiteboard" (:block/type page)) :whiteboard :page)
+        (route-handler/redirect! {:to          (if (model/whiteboard-page? page) :whiteboard :page)
                                   :push        false
                                   :push        false
                                   :path-params {:name new-page-name}}))
                                   :path-params {:name new-page-name}}))
 
 

+ 1 - 1
src/main/frontend/mobile/index.css

@@ -9,7 +9,7 @@
   flex: 0 0 auto;
   flex: 0 0 auto;
   white-space: nowrap;
   white-space: nowrap;
   height: 80px;
   height: 80px;
-  align-items: start;
+  align-items: flex-start;
   box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05);
   box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05);
 
 
   .bottom-action {
   .bottom-action {

+ 3 - 2
src/main/frontend/modules/file/core.cljs

@@ -8,7 +8,8 @@
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util.property :as property]
             [frontend.util.property :as property]
             [frontend.util.fs :as fs-util]
             [frontend.util.fs :as fs-util]
-            [frontend.handler.file :as file-handler]))
+            [frontend.handler.file :as file-handler]
+            [frontend.db.model :as model]))
 
 
 (defn- indented-block-content
 (defn- indented-block-content
   [content spaces-tabs]
   [content spaces-tabs]
@@ -111,7 +112,7 @@
       (let [format (name (get page :block/format
       (let [format (name (get page :block/format
                               (state/get-preferred-format)))
                               (state/get-preferred-format)))
             title (string/capitalize (:block/name page))
             title (string/capitalize (:block/name page))
-            whiteboard-page? (= "whiteboard" (:block/type page))
+            whiteboard-page? (model/whiteboard-page? page)
             format (if whiteboard-page? "edn" format)
             format (if whiteboard-page? "edn" format)
             journal-page? (date/valid-journal-title? title)
             journal-page? (date/valid-journal-title? title)
             journal-title (date/normalize-journal-title title)
             journal-title (date/normalize-journal-title title)

+ 7 - 7
src/main/frontend/ui.cljs

@@ -941,19 +941,19 @@
      (let [^js jsTablerIcons (gobj/get js/window "tablerIcons")]
      (let [^js jsTablerIcons (gobj/get js/window "tablerIcons")]
        (if (or extension? font? (not jsTablerIcons))
        (if (or extension? font? (not jsTablerIcons))
          [:span.ui__icon (merge {:class
          [:span.ui__icon (merge {:class
-                     (util/format
-                      (str "%s-" class
-                           (when (:class opts)
-                             (str " " (string/trim (:class opts)))))
-                      (if extension? "tie tie" "ti ti"))}
-                    (dissoc opts :class :extension? :font?))]
+                                 (util/format
+                                  (str "%s-" class
+                                       (when (:class opts)
+                                         (str " " (string/trim (:class opts)))))
+                                  (if extension? "tie tie" "ti ti"))}
+                                (dissoc opts :class :extension? :font?))]
 
 
          ;; tabler svg react
          ;; tabler svg react
          (when-let [klass (gobj/get js/tablerIcons (str "Icon" (csk/->PascalCase class)))]
          (when-let [klass (gobj/get js/tablerIcons (str "Icon" (csk/->PascalCase class)))]
            (let [f (get-adapt-icon-class klass)]
            (let [f (get-adapt-icon-class klass)]
              [:span.ui__icon.ti
              [:span.ui__icon.ti
               {:class (str "ls-icon-" class)}
               {:class (str "ls-icon-" class)}
-              (f (merge {:size 18} (r/map-keys->camel-case opts)))])))))))
+              (f (merge {:size 18} (r/map-keys->camel-case (dissoc opts :class))))])))))))
 
 
 (defn button
 (defn button
   [text & {:keys [background href class intent on-click small? large? title icon icon-props disabled?]
   [text & {:keys [background href class intent on-click small? large? title icon icon-props disabled?]

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

@@ -84,6 +84,15 @@ export const DevTools = observer(() => {
     .map(p => p.join(''))
     .map(p => p.join(''))
     .join('|')
     .join('|')
 
 
+  const originPoint = canvasAnchorRef.current
+    ? ReactDOM.createPortal(
+        <svg className="tl-renderer-dev-tools tl-grid">
+          <circle cx={point[0] * zoom} cy={point[1] * zoom} r="4" fill="red" />
+        </svg>,
+        canvasAnchorRef.current
+      )
+    : null
+
   const rendererStatus = statusbarAnchorRef.current
   const rendererStatus = statusbarAnchorRef.current
     ? ReactDOM.createPortal(
     ? ReactDOM.createPortal(
         <div
         <div
@@ -101,6 +110,7 @@ export const DevTools = observer(() => {
 
 
   return (
   return (
     <>
     <>
+      {originPoint}
       {rendererStatus}
       {rendererStatus}
       <HistoryStack />
       <HistoryStack />
     </>
     </>

+ 2 - 24
tldraw/apps/tldraw-logseq/src/components/StatusBar/StatusBar.tsx

@@ -1,38 +1,16 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import * as React from 'react'
-import { observer } from 'mobx-react-lite'
 import { useApp } from '@tldraw/react'
 import { useApp } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
 import type { Shape } from '../../lib'
 import type { Shape } from '../../lib'
 
 
 export const StatusBar = observer(function StatusBar() {
 export const StatusBar = observer(function StatusBar() {
   const app = useApp<Shape>()
   const app = useApp<Shape>()
-  React.useEffect(() => {
-    const canvas = document.querySelector<HTMLElement>('.logseq-tldraw-wrapper .tl-canvas')
-    const actionBar = document.querySelector<HTMLElement>('.logseq-tldraw-wrapper .tl-action-bar')
-    if (canvas) {
-      canvas.style.height = 'calc(100% - 32px)'
-    }
-
-    if (actionBar) {
-      actionBar.style.marginBottom = '32px'
-    }
-
-    return () => {
-      if (canvas) {
-        canvas.style.height = '100%'
-      }
-
-      if (actionBar) {
-        actionBar.style.marginBottom = '0px'
-      }
-    }
-  })
   return (
   return (
     <div className="tl-statusbar">
     <div className="tl-statusbar">
       {app.selectedTool.id} | {app.selectedTool.currentState.id}
       {app.selectedTool.id} | {app.selectedTool.currentState.id}
       <div style={{ flex: 1 }} />
       <div style={{ flex: 1 }} />
-      <div id="tl-statusbar-anchor" style={{ display: 'flex' }} />
+      <div id="tl-statusbar-anchor" className='flex gap-1' />
     </div>
     </div>
   )
   )
 })
 })

+ 5 - 3
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -377,13 +377,15 @@ export function usePaste() {
         }
         }
       })
       })
 
 
+      const filesOnly = dataTransfer?.types.every(t => t === 'Files')
+
       app.wrapUpdate(() => {
       app.wrapUpdate(() => {
         const allAssets = [...imageAssetsToCreate, ...assetsToClone]
         const allAssets = [...imageAssetsToCreate, ...assetsToClone]
         if (allAssets.length > 0) {
         if (allAssets.length > 0) {
           app.createAssets(allAssets)
           app.createAssets(allAssets)
         }
         }
-        if (newShapes.length > 0) {
-          app.createShapes(newShapes)
+        if (allShapesToAdd.length > 0) {
+          app.createShapes(allShapesToAdd)
         }
         }
         app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
         app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
 
 
@@ -397,7 +399,7 @@ export function usePaste() {
         app.selectedTool.transition('idle') // clears possible editing states
         app.selectedTool.transition('idle') // clears possible editing states
         app.cursors.setCursor(TLCursor.Default)
         app.cursors.setCursor(TLCursor.Default)
 
 
-        if (fromDrop) {
+        if (fromDrop || filesOnly) {
           app.packIntoRectangle()
           app.packIntoRectangle()
         }
         }
       })
       })

+ 8 - 8
tldraw/apps/tldraw-logseq/src/lib/preview-manager.tsx

@@ -33,7 +33,7 @@ export class PreviewManager {
     })
     })
   }
   }
 
 
-  generatePreviewJsx(viewport?: TLViewport) {
+  generatePreviewJsx(viewport?: TLViewport, ratio?: number) {
     const allBounds = [...(this.shapes ?? []).map(s => s.getRotatedBounds())]
     const allBounds = [...(this.shapes ?? []).map(s => s.getRotatedBounds())]
     const vBounds = viewport?.currentView
     const vBounds = viewport?.currentView
     if (vBounds) {
     if (vBounds) {
@@ -47,7 +47,7 @@ export class PreviewManager {
     commonBounds = BoundsUtils.expandBounds(commonBounds, SVG_EXPORT_PADDING)
     commonBounds = BoundsUtils.expandBounds(commonBounds, SVG_EXPORT_PADDING)
 
 
     // make sure commonBounds is of ratio 4/3 (should we have another ratio setting?)
     // make sure commonBounds is of ratio 4/3 (should we have another ratio setting?)
-    commonBounds = viewport ? BoundsUtils.ensureRatio(commonBounds, 4 / 3) : commonBounds
+    commonBounds = ratio ? BoundsUtils.ensureRatio(commonBounds, ratio) : commonBounds
 
 
     const translatePoint = (p: [number, number]): [string, string] => {
     const translatePoint = (p: [number, number]): [string, string] => {
       return [(p[0] - commonBounds.minX).toFixed(2), (p[1] - commonBounds.minY).toFixed(2)]
       return [(p[0] - commonBounds.minX).toFixed(2), (p[1] - commonBounds.minY).toFixed(2)]
@@ -123,8 +123,8 @@ export class PreviewManager {
     return svgElement
     return svgElement
   }
   }
 
 
-  exportAsSVG() {
-    const svgElement = this.generatePreviewJsx()
+  exportAsSVG(ratio: number) {
+    const svgElement = this.generatePreviewJsx(undefined, ratio)
     return svgElement ? ReactDOMServer.renderToString(svgElement) : ''
     return svgElement ? ReactDOMServer.renderToString(svgElement) : ''
   }
   }
 }
 }
@@ -134,12 +134,12 @@ export class PreviewManager {
  *
  *
  * @param serializedApp
  * @param serializedApp
  */
  */
-export function generateSVGFromApp(serializedApp: TLDocumentModel<Shape>) {
+export function generateSVGFromModel(serializedApp: TLDocumentModel<Shape>, ratio = 4 / 3) {
   const preview = new PreviewManager(serializedApp)
   const preview = new PreviewManager(serializedApp)
-  return preview.exportAsSVG()
+  return preview.exportAsSVG(ratio)
 }
 }
 
 
-export function generateJSXFromApp(serializedApp: TLDocumentModel<Shape>) {
+export function generateJSXFromModel(serializedApp: TLDocumentModel<Shape>, ratio = 4 / 3) {
   const preview = new PreviewManager(serializedApp)
   const preview = new PreviewManager(serializedApp)
-  return preview.generatePreviewJsx()
+  return preview.generatePreviewJsx(undefined, ratio)
 }
 }

+ 8 - 24
tldraw/apps/tldraw-logseq/src/lib/shapes/ImageShape.tsx

@@ -1,8 +1,8 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import * as React from 'react'
+import { TLAsset, TLImageShape, TLImageShapeProps } from '@tldraw/core'
 import { HTMLContainer, TLComponentProps } from '@tldraw/react'
 import { HTMLContainer, TLComponentProps } from '@tldraw/react'
-import { isSafari, TLAsset, TLImageShape, TLImageShapeProps } from '@tldraw/core'
 import { observer } from 'mobx-react-lite'
 import { observer } from 'mobx-react-lite'
+import * as React from 'react'
 import { LogseqContext } from '../logseq-context'
 import { LogseqContext } from '../logseq-context'
 import { BindingIndicator } from './BindingIndicator'
 import { BindingIndicator } from './BindingIndicator'
 
 
@@ -80,10 +80,6 @@ export class ImageShape extends TLImageShape<ImageShapeProps> {
   })
   })
 
 
   getShapeSVGJsx({ assets }: { assets: TLAsset[] }) {
   getShapeSVGJsx({ assets }: { assets: TLAsset[] }) {
-    if (isSafari()) {
-      // Safari doesn't support foreignObject well
-      return super.getShapeSVGJsx(null);
-    }
     // Do not need to consider the original point here
     // Do not need to consider the original point here
     const bounds = this.getBounds()
     const bounds = this.getBounds()
     const {
     const {
@@ -94,6 +90,7 @@ export class ImageShape extends TLImageShape<ImageShapeProps> {
     const asset = assets.find(ass => ass.id === assetId)
     const asset = assets.find(ass => ass.id === assetId)
 
 
     if (asset) {
     if (asset) {
+      // TODO: add clipping
       const [t, r, b, l] = Array.isArray(clipping)
       const [t, r, b, l] = Array.isArray(clipping)
         ? clipping
         ? clipping
         : [clipping, clipping, clipping, clipping]
         : [clipping, clipping, clipping, clipping]
@@ -101,24 +98,11 @@ export class ImageShape extends TLImageShape<ImageShapeProps> {
       const make_asset_url = window.logseq?.api?.make_asset_url
       const make_asset_url = window.logseq?.api?.make_asset_url
 
 
       return (
       return (
-        <g>
-          <foreignObject width={bounds.width} height={bounds.height}>
-            <img
-              src={make_asset_url ? make_asset_url(asset.src) : asset.src}
-              draggable={false}
-              loading="lazy"
-              style={{
-                position: 'relative',
-                top: -t,
-                left: -l,
-                width: w + (l - r),
-                height: h + (t - b),
-                objectFit: this.props.objectFit,
-                pointerEvents: 'all',
-              }}
-            />
-          </foreignObject>
-        </g>
+        <image
+          width={bounds.width}
+          height={bounds.height}
+          href={make_asset_url ? make_asset_url(asset.src) : asset.src}
+        />
       )
       )
     } else {
     } else {
       return super.getShapeSVGJsx({})
       return super.getShapeSVGJsx({})

+ 23 - 34
tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx

@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import { isSafari, TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
+import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
 import { HTMLContainer, TLComponentProps } from '@tldraw/react'
 import { HTMLContainer, TLComponentProps } from '@tldraw/react'
 import { action, computed } from 'mobx'
 import { action, computed } from 'mobx'
 import { observer } from 'mobx-react-lite'
 import { observer } from 'mobx-react-lite'
@@ -131,10 +131,6 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
   }
   }
 
 
   getShapeSVGJsx() {
   getShapeSVGJsx() {
-    if (isSafari()) {
-      // Safari doesn't support foreignObject well
-      return super.getShapeSVGJsx(null);
-    }
     // Do not need to consider the original point here
     // Do not need to consider the original point here
     const bounds = this.getBounds()
     const bounds = this.getBounds()
     const embedId = this.embedId
     const embedId = this.embedId
@@ -142,35 +138,28 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
     if (embedId) {
     if (embedId) {
       return (
       return (
         <g>
         <g>
-          <foreignObject width={bounds.width} height={bounds.height}>
-            <img
-              src={`https://img.youtube.com/vi/${embedId}/mqdefault.jpg`}
-              draggable={false}
-              style={{
-                display: 'contents',
-                width: bounds.width,
-                height: bounds.height,
-              }}
-              loading="lazy"
-              className="rounded-lg relative pointer-events-none w-full h-full grayscale-[50%]"
-            />
-            <div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
-              <svg
-                width="240"
-                height="240"
-                viewBox="0 0 15 15"
-                fill="none"
-                xmlns="http://www.w3.org/2000/svg"
-              >
-                <path
-                  d="M4.76447 3.12199C5.63151 3.04859 6.56082 3 7.5 3C8.43918 3 9.36849 3.04859 10.2355 3.12199C11.2796 3.21037 11.9553 3.27008 12.472 3.39203C12.9425 3.50304 13.2048 3.64976 13.4306 3.88086C13.4553 3.90618 13.4902 3.94414 13.5133 3.97092C13.7126 4.20149 13.8435 4.4887 13.918 5.03283C13.9978 5.6156 14 6.37644 14 7.52493C14 8.66026 13.9978 9.41019 13.9181 9.98538C13.8439 10.5206 13.7137 10.8061 13.5125 11.0387C13.4896 11.0651 13.4541 11.1038 13.4296 11.1287C13.2009 11.3625 12.9406 11.5076 12.4818 11.6164C11.9752 11.7365 11.3143 11.7942 10.2878 11.8797C9.41948 11.9521 8.47566 12 7.5 12C6.52434 12 5.58052 11.9521 4.7122 11.8797C3.68572 11.7942 3.02477 11.7365 2.51816 11.6164C2.05936 11.5076 1.7991 11.3625 1.57037 11.1287C1.54593 11.1038 1.51035 11.0651 1.48748 11.0387C1.28628 10.8061 1.15612 10.5206 1.08193 9.98538C1.00221 9.41019 1 8.66026 1 7.52493C1 6.37644 1.00216 5.6156 1.082 5.03283C1.15654 4.4887 1.28744 4.20149 1.48666 3.97092C1.5098 3.94414 1.54468 3.90618 1.56942 3.88086C1.7952 3.64976 2.05752 3.50304 2.52796 3.39203C3.04473 3.27008 3.7204 3.21037 4.76447 3.12199ZM0 7.52493C0 5.28296 0 4.16198 0.729985 3.31713C0.766457 3.27491 0.815139 3.22194 0.854123 3.18204C1.63439 2.38339 2.64963 2.29744 4.68012 2.12555C5.56923 2.05028 6.52724 2 7.5 2C8.47276 2 9.43077 2.05028 10.3199 2.12555C12.3504 2.29744 13.3656 2.38339 14.1459 3.18204C14.1849 3.22194 14.2335 3.27491 14.27 3.31713C15 4.16198 15 5.28296 15 7.52493C15 9.74012 15 10.8477 14.2688 11.6929C14.2326 11.7348 14.1832 11.7885 14.1444 11.8281C13.3629 12.6269 12.3655 12.71 10.3709 12.8763C9.47971 12.9505 8.50782 13 7.5 13C6.49218 13 5.52028 12.9505 4.62915 12.8763C2.63446 12.71 1.63712 12.6269 0.855558 11.8281C0.816844 11.7885 0.767442 11.7348 0.731221 11.6929C0 10.8477 0 9.74012 0 7.52493ZM5.25 5.38264C5.25 5.20225 5.43522 5.08124 5.60041 5.15369L10.428 7.27105C10.6274 7.35853 10.6274 7.64147 10.428 7.72895L5.60041 9.84631C5.43522 9.91876 5.25 9.79775 5.25 9.61736V5.38264Z"
-                  fill="#D10014"
-                  fill-rule="evenodd"
-                  clip-rule="evenodd"
-                ></path>
-              </svg>
-            </div>
-          </foreignObject>
+          <image
+            width={bounds.width}
+            height={bounds.height}
+            href={`https://img.youtube.com/vi/${embedId}/mqdefault.jpg`}
+            className="grayscale-[50%]"
+          />
+          <svg
+            x={bounds.width / 4}
+            y={bounds.height / 4}
+            width={bounds.width / 2}
+            height={bounds.height / 2}
+            viewBox="0 0 15 15"
+            fill="none"
+            xmlns="http://www.w3.org/2000/svg"
+          >
+            <path
+              d="M4.76447 3.12199C5.63151 3.04859 6.56082 3 7.5 3C8.43918 3 9.36849 3.04859 10.2355 3.12199C11.2796 3.21037 11.9553 3.27008 12.472 3.39203C12.9425 3.50304 13.2048 3.64976 13.4306 3.88086C13.4553 3.90618 13.4902 3.94414 13.5133 3.97092C13.7126 4.20149 13.8435 4.4887 13.918 5.03283C13.9978 5.6156 14 6.37644 14 7.52493C14 8.66026 13.9978 9.41019 13.9181 9.98538C13.8439 10.5206 13.7137 10.8061 13.5125 11.0387C13.4896 11.0651 13.4541 11.1038 13.4296 11.1287C13.2009 11.3625 12.9406 11.5076 12.4818 11.6164C11.9752 11.7365 11.3143 11.7942 10.2878 11.8797C9.41948 11.9521 8.47566 12 7.5 12C6.52434 12 5.58052 11.9521 4.7122 11.8797C3.68572 11.7942 3.02477 11.7365 2.51816 11.6164C2.05936 11.5076 1.7991 11.3625 1.57037 11.1287C1.54593 11.1038 1.51035 11.0651 1.48748 11.0387C1.28628 10.8061 1.15612 10.5206 1.08193 9.98538C1.00221 9.41019 1 8.66026 1 7.52493C1 6.37644 1.00216 5.6156 1.082 5.03283C1.15654 4.4887 1.28744 4.20149 1.48666 3.97092C1.5098 3.94414 1.54468 3.90618 1.56942 3.88086C1.7952 3.64976 2.05752 3.50304 2.52796 3.39203C3.04473 3.27008 3.7204 3.21037 4.76447 3.12199ZM0 7.52493C0 5.28296 0 4.16198 0.729985 3.31713C0.766457 3.27491 0.815139 3.22194 0.854123 3.18204C1.63439 2.38339 2.64963 2.29744 4.68012 2.12555C5.56923 2.05028 6.52724 2 7.5 2C8.47276 2 9.43077 2.05028 10.3199 2.12555C12.3504 2.29744 13.3656 2.38339 14.1459 3.18204C14.1849 3.22194 14.2335 3.27491 14.27 3.31713C15 4.16198 15 5.28296 15 7.52493C15 9.74012 15 10.8477 14.2688 11.6929C14.2326 11.7348 14.1832 11.7885 14.1444 11.8281C13.3629 12.6269 12.3655 12.71 10.3709 12.8763C9.47971 12.9505 8.50782 13 7.5 13C6.49218 13 5.52028 12.9505 4.62915 12.8763C2.63446 12.71 1.63712 12.6269 0.855558 11.8281C0.816844 11.7885 0.767442 11.7348 0.731221 11.6929C0 10.8477 0 9.74012 0 7.52493ZM5.25 5.38264C5.25 5.20225 5.43522 5.08124 5.60041 5.15369L10.428 7.27105C10.6274 7.35853 10.6274 7.64147 10.428 7.72895L5.60041 9.84631C5.43522 9.91876 5.25 9.79775 5.25 9.61736V5.38264Z"
+              fill="#D10014"
+              fillRule="evenodd"
+              clipRule="evenodd"
+            ></path>
+          </svg>
         </g>
         </g>
       )
       )
     }
     }

+ 2 - 1
tldraw/apps/tldraw-logseq/src/styles.css

@@ -679,9 +679,10 @@ button.tl-select-input-trigger {
   opacity: 0.5;
   opacity: 0.5;
 }
 }
 
 
+/* This will breaks block selection
 [data-inner-events=false] * {
 [data-inner-events=false] * {
   user-select: none;
   user-select: none;
-}
+} */
 
 
 .tl-logseq-portal-container {
 .tl-logseq-portal-container {
   @apply flex flex-col rounded-lg absolute;
   @apply flex flex-col rounded-lg absolute;

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

@@ -1,3 +1,5 @@
+const rootTailwindConfig = require('../../tailwind.config')
+
 module.exports = {
 module.exports = {
   plugins: {
   plugins: {
     'postcss-import': {},
     'postcss-import': {},
@@ -5,6 +7,7 @@ module.exports = {
     'postcss-import-ext-glob': {},
     'postcss-import-ext-glob': {},
     'tailwindcss/nesting': {},
     'tailwindcss/nesting': {},
     tailwindcss: {
     tailwindcss: {
+      ...rootTailwindConfig,
       content: ['./**/*.jsx', '../apps/**/*.{js,jsx,ts,tsx}'],
       content: ['./**/*.jsx', '../apps/**/*.{js,jsx,ts,tsx}'],
     },
     },
     autoprefixer: {},
     autoprefixer: {},

+ 68 - 15
tldraw/demo/src/App.jsx

@@ -1,7 +1,7 @@
 import { uniqueId, fileToBase64 } from '@tldraw/core'
 import { uniqueId, fileToBase64 } from '@tldraw/core'
 import React from 'react'
 import React from 'react'
 import ReactDOM from 'react-dom'
 import ReactDOM from 'react-dom'
-import { App as TldrawApp } from '@tldraw/logseq'
+import { App as TldrawApp, generateJSXFromModel } from '@tldraw/logseq'
 
 
 const storingKey = 'playground.index'
 const storingKey = 'playground.index'
 
 
@@ -83,16 +83,17 @@ const PageNameLink = props => {
   )
   )
 }
 }
 
 
-const ThemeSwitcher = ({ theme, setTheme }) => {
+const StatusBarSwitcher = ({ label, onClick }) => {
   const [anchor, setAnchor] = React.useState(null)
   const [anchor, setAnchor] = React.useState(null)
   React.useEffect(() => {
   React.useEffect(() => {
     if (anchor) {
     if (anchor) {
       return
       return
     }
     }
-    let el = document.querySelector('#theme-switcher')
+    const id = 'status-bar-switcher-' + uniqueId()
+    let el = document.getElementById(id)
     if (!el) {
     if (!el) {
       el = document.createElement('div')
       el = document.createElement('div')
-      el.id = 'theme-switcher'
+      el.id = id
       let timer = setInterval(() => {
       let timer = setInterval(() => {
         const statusBarAnchor = document.querySelector('#tl-statusbar-anchor')
         const statusBarAnchor = document.querySelector('#tl-statusbar-anchor')
         if (statusBarAnchor) {
         if (statusBarAnchor) {
@@ -104,26 +105,76 @@ const ThemeSwitcher = ({ theme, setTheme }) => {
     }
     }
   })
   })
 
 
-  React.useEffect(() => {
-    document.documentElement.setAttribute('data-theme', theme)
-  }, [theme])
-
   if (!anchor) {
   if (!anchor) {
     return null
     return null
   }
   }
 
 
   return ReactDOM.createPortal(
   return ReactDOM.createPortal(
     <button
     <button
-      className="flex items-center justify-center mx-2 bg-grey"
+      className="flex items-center justify-center bg-grey border px-1"
       style={{ fontSize: '1em' }}
       style={{ fontSize: '1em' }}
-      onClick={() => setTheme(t => (t === 'dark' ? 'light' : 'dark'))}
+      onClick={onClick}
     >
     >
-      {theme} theme
+      {label}
     </button>,
     </button>,
     anchor
     anchor
   )
   )
 }
 }
 
 
+const ThemeSwitcher = () => {
+  const [theme, setTheme] = React.useState('light')
+
+  React.useEffect(() => {
+    document.documentElement.setAttribute('data-theme', theme)
+  }, [theme])
+
+  return (
+    <StatusBarSwitcher
+      label={theme + ' theme'}
+      onClick={() => {
+        setTheme(t => (t === 'dark' ? 'light' : 'dark'))
+      }}
+    />
+  )
+}
+
+const PreviewButton = ({ model }) => {
+  const [show, setShow] = React.useState(false)
+
+  const [[w, h], setSize] = React.useState([window.innerWidth, window.innerHeight])
+
+  React.useEffect(() => {
+    const onResize = () => {
+      setSize([window.innerWidth, window.innerHeight])
+    }
+    window.addEventListener('resize', onResize)
+    return () => window.removeEventListener('resize', onResize)
+  }, [])
+
+  const preview = React.useMemo(() => {
+    return show ? generateJSXFromModel(model, w / h) : null
+  }, [show, model, w, h])
+
+  return (
+    <>
+      {preview ? (
+        <div
+          className="fixed inset-0 flex items-center justify-center pointer-events-none h-screen w-screen"
+          style={{ zIndex: '10000' }}
+        >
+          <div className="w-1/2 h-1/2 border bg-white">{preview}</div>
+        </div>
+      ) : null}
+      <StatusBarSwitcher
+        label="Preview"
+        onClick={() => {
+          setShow(s => !s)
+        }}
+      />
+    </>
+  )
+}
+
 const searchHandler = q => {
 const searchHandler = q => {
   return Promise.resolve({
   return Promise.resolve({
     pages: ['foo', 'bar', 'asdf'].filter(p => p.includes(q)),
     pages: ['foo', 'bar', 'asdf'].filter(p => p.includes(q)),
@@ -136,8 +187,6 @@ const searchHandler = q => {
 }
 }
 
 
 export default function App() {
 export default function App() {
-  const [theme, setTheme] = React.useState('light')
-
   const [model, setModel] = React.useState(documentModel)
   const [model, setModel] = React.useState(documentModel)
 
 
   // Mimic external reload event
   // Mimic external reload event
@@ -153,7 +202,8 @@ export default function App() {
 
 
   return (
   return (
     <div className={`h-screen w-screen`}>
     <div className={`h-screen w-screen`}>
-      <ThemeSwitcher theme={theme} setTheme={setTheme} />
+      <ThemeSwitcher />
+      <PreviewButton model={model} />
       <TldrawApp
       <TldrawApp
         renderers={{
         renderers={{
           Page,
           Page,
@@ -170,7 +220,10 @@ export default function App() {
           makeAssetUrl: a => a,
           makeAssetUrl: a => a,
         }}
         }}
         model={model}
         model={model}
-        onPersist={onPersist}
+        onPersist={app => {
+          onPersist(app)
+          setModel(app.serialized)
+        }}
       />
       />
     </div>
     </div>
   )
   )

+ 1 - 0
tldraw/packages/core/src/lib/TLSettings.ts

@@ -1,5 +1,6 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import { observable, makeObservable, action } from 'mobx'
 import { observable, makeObservable, action } from 'mobx'
+import { isSafari } from '../utils'
 
 
 export interface TLSettingsProps {
 export interface TLSettingsProps {
   mode: 'light' | 'dark'
   mode: 'light' | 'dark'

+ 1 - 1
tldraw/packages/core/src/utils/ColorUtils.ts

@@ -1,7 +1,7 @@
 import { Color } from '../types'
 import { Color } from '../types'
 
 
 export function getComputedColor(color: string, type: string): string {
 export function getComputedColor(color: string, type: string): string {
-  if (Object.values(Color).includes(color)) {
+  if (Object.values(Color).includes(color as Color) || color == null) {
     return `var(--ls-wb-${type}-color-${color ? color : 'default'})`
     return `var(--ls-wb-${type}-color-${color ? color : 'default'})`
   }
   }
 
 

+ 1 - 0
tldraw/packages/react/package.json

@@ -41,6 +41,7 @@
     "mobx": "^6.7.0",
     "mobx": "^6.7.0",
     "mobx-react-lite": "^3.4.0",
     "mobx-react-lite": "^3.4.0",
     "mousetrap": "^1.6.5",
     "mousetrap": "^1.6.5",
+    "polished": "^4.2.2",
     "rbush": "^3.0.1",
     "rbush": "^3.0.1",
     "uuid": "^9.0.0"
     "uuid": "^9.0.0"
   },
   },

+ 12 - 4
tldraw/packages/react/src/components/ui/Grid/Grid.tsx

@@ -1,4 +1,4 @@
-import { modulate } from '@tldraw/core'
+import { modulate, clamp } from '@tldraw/core'
 import { observer } from 'mobx-react-lite'
 import { observer } from 'mobx-react-lite'
 import { useRendererContext } from '../../../hooks'
 import { useRendererContext } from '../../../hooks'
 import type { TLGridProps } from '../../../types'
 import type { TLGridProps } from '../../../types'
@@ -10,7 +10,7 @@ const STEPS = [
   [0.7, 2.5, 1],
   [0.7, 2.5, 1],
 ]
 ]
 
 
-export const Grid = observer(function Grid({ size }: TLGridProps) {
+const SVGGrid = observer(function CanvasGrid({ size }: TLGridProps) {
   const {
   const {
     viewport: {
     viewport: {
       camera: { point, zoom },
       camera: { point, zoom },
@@ -25,7 +25,9 @@ export const Grid = observer(function Grid({ size }: TLGridProps) {
           const yo = point[1] * zoom
           const yo = point[1] * zoom
           const gxo = xo > 0 ? xo % s : s + (xo % s)
           const gxo = xo > 0 ? xo % s : s + (xo % s)
           const gyo = yo > 0 ? yo % s : s + (yo % s)
           const gyo = yo > 0 ? yo % s : s + (yo % s)
-          const opacity = zoom < mid ? modulate(zoom, [min, mid], [0, 1]) : 1
+          const opacity = modulate(zoom, [min, mid], [0, 1])
+
+          const hide = opacity > 2 || opacity < 0.1
 
 
           return (
           return (
             <pattern
             <pattern
@@ -35,7 +37,9 @@ export const Grid = observer(function Grid({ size }: TLGridProps) {
               height={s}
               height={s}
               patternUnits="userSpaceOnUse"
               patternUnits="userSpaceOnUse"
             >
             >
-              <circle className={`tl-grid-dot`} cx={gxo} cy={gyo} r={1.5} opacity={opacity} />
+              {!hide && (
+                <circle className={`tl-grid-dot`} cx={gxo} cy={gyo} r={1.5} opacity={clamp(opacity, 0, 1)} />
+              )}
             </pattern>
             </pattern>
           )
           )
         })}
         })}
@@ -46,3 +50,7 @@ export const Grid = observer(function Grid({ size }: TLGridProps) {
     </svg>
     </svg>
   )
   )
 })
 })
+
+export const Grid = observer(function Grid({ size }: TLGridProps) {
+  return <SVGGrid size={size} />
+})

+ 4 - 1
tldraw/packages/react/src/hooks/useGestureEvents.ts

@@ -15,6 +15,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
 
 
   const rOriginPoint = React.useRef<number[] | undefined>(undefined)
   const rOriginPoint = React.useRef<number[] | undefined>(undefined)
   const rDelta = React.useRef<number[]>([0, 0])
   const rDelta = React.useRef<number[]>([0, 0])
+  const rWheelTs = React.useRef<number>(0)
 
 
   const events = React.useMemo(() => {
   const events = React.useMemo(() => {
     const onWheel: Handler<'wheel', WheelEvent> = gesture => {
     const onWheel: Handler<'wheel', WheelEvent> = gesture => {
@@ -23,10 +24,12 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
 
 
       const [x, y, z] = normalizeWheel(event)
       const [x, y, z] = normalizeWheel(event)
 
 
-      if (inputs.state === 'pinching') {
+      if (inputs.state === 'pinching' || rWheelTs.current >= event.timeStamp) {
         return
         return
       }
       }
 
 
+      rWheelTs.current = event.timeStamp
+
       if ((event.altKey || event.ctrlKey || event.metaKey) && event.buttons === 0) {
       if ((event.altKey || event.ctrlKey || event.metaKey) && event.buttons === 0) {
         const bounds = viewport.bounds
         const bounds = viewport.bounds
         const point = inputs.currentScreenPoint ?? [bounds.width / 2, bounds.height / 2]
         const point = inputs.currentScreenPoint ?? [bounds.width / 2, bounds.height / 2]

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

@@ -421,6 +421,13 @@ const tlcss = css`
     color: var(--tl-background);
     color: var(--tl-background);
   }
   }
 
 
+  .tl-grid-canvas {
+    position: absolute;
+    touch-action: none;
+    pointer-events: none;
+    user-select: none;
+  }
+
   .tl-grid {
   .tl-grid {
     position: absolute;
     position: absolute;
     width: 100%;
     width: 100%;

+ 1 - 1
tldraw/yarn.lock

@@ -4001,7 +4001,7 @@ pirates@^4.0.1:
   resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
   resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
   integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
   integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
 
 
-polished@^4.0.0:
+polished@^4.0.0, polished@^4.2.2:
   version "4.2.2"
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
   resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
   integrity sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==
   integrity sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==