Bläddra i källkod

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

fix(whiteboard): whiteboard ux issues
Gabriel Horner 2 år sedan
förälder
incheckning
962998a7b2
30 ändrade filer med 231 tillägg och 170 borttagningar
  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)]
                          (cond
                            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
                              ;; 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
                            (if (db-model/get-block-by-uuid block-id)

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

@@ -2534,7 +2534,7 @@
                             (filterv identity)
                             (map (fn [x] (if (vector? 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 "⋯"])))
                             (interpose (breadcrumb-separator)))]
         [:div.breadcrumb.block-parents.flex-row.flex-1
@@ -3603,7 +3603,7 @@
             (when (seq blocks)
               (let [alias? (:block/alias? 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))}
                              (:ref? config)
                              (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)]
       [:.menu-links-wrapper
        (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
 ;; 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)
           fmt-journal? (boolean (date/journal-title->int page-name))
           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
           page (if block?
                  (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
@@ -401,7 +401,7 @@
               {:key path-page-name
                :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.relative
           (when (and (not sidebar?) (not block?))

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

@@ -113,7 +113,8 @@
             {:title   (t :page/delete)
              :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)
              :options {:on-click (fn []
                                    (state/sidebar-add-block!

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

@@ -2,6 +2,7 @@
   (:require [cljs-drag-n-drop.core :as dnd]
             [clojure.string :as string]
             [frontend.components.command-palette :as command-palette]
+            [frontend.components.find-in-page :as find-in-page]
             [frontend.components.header :as header]
             [frontend.components.journal :as journal]
             [frontend.components.onboarding :as onboarding]
@@ -12,7 +13,6 @@
             [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
-            [frontend.components.find-in-page :as find-in-page]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
@@ -392,7 +392,9 @@
                             (when-let [id (state/get-edit-input-id)]
                               (let [format (:block/format (state/get-edit-block))]
                                 (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)}
   [{: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?)

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

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

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

@@ -74,7 +74,23 @@
                 :min-height "40px"
                 :max-height "420px"
                 :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
   ([page-name classname]
@@ -82,24 +98,15 @@
   ([page-name classname render-fn]
    (let [page-entity (model/get-page page-name)
          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)
-       (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
   [page-name]

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

@@ -1677,13 +1677,21 @@
    macro-name))
 
 (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
   [repo]

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

@@ -18,7 +18,7 @@
 
 (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
   [props]

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

@@ -61,7 +61,8 @@
     (gp-util/safe-subs s 0 200)))
 
 (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]
    (when page-name
      (let [page-name (util/page-name-sanity-lc page-name)]
@@ -475,7 +476,7 @@
 
       ;; Redirect to the newly renamed page
       (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
                                   :path-params {:name new-page-name}}))
 

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

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

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

@@ -8,7 +8,8 @@
             [frontend.state :as state]
             [frontend.util.property :as property]
             [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
   [content spaces-tabs]
@@ -111,7 +112,7 @@
       (let [format (name (get page :block/format
                               (state/get-preferred-format)))
             title (string/capitalize (:block/name page))
-            whiteboard-page? (= "whiteboard" (:block/type page))
+            whiteboard-page? (model/whiteboard-page? page)
             format (if whiteboard-page? "edn" format)
             journal-page? (date/valid-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")]
        (if (or extension? font? (not jsTablerIcons))
          [: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
          (when-let [klass (gobj/get js/tablerIcons (str "Icon" (csk/->PascalCase class)))]
            (let [f (get-adapt-icon-class klass)]
              [:span.ui__icon.ti
               {: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
   [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(''))
     .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
     ? ReactDOM.createPortal(
         <div
@@ -101,6 +110,7 @@ export const DevTools = observer(() => {
 
   return (
     <>
+      {originPoint}
       {rendererStatus}
       <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-explicit-any */
-import * as React from 'react'
-import { observer } from 'mobx-react-lite'
 import { useApp } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
 import type { Shape } from '../../lib'
 
 export const StatusBar = observer(function StatusBar() {
   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 (
     <div className="tl-statusbar">
       {app.selectedTool.id} | {app.selectedTool.currentState.id}
       <div style={{ flex: 1 }} />
-      <div id="tl-statusbar-anchor" style={{ display: 'flex' }} />
+      <div id="tl-statusbar-anchor" className='flex gap-1' />
     </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(() => {
         const allAssets = [...imageAssetsToCreate, ...assetsToClone]
         if (allAssets.length > 0) {
           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])))
 
@@ -397,7 +399,7 @@ export function usePaste() {
         app.selectedTool.transition('idle') // clears possible editing states
         app.cursors.setCursor(TLCursor.Default)
 
-        if (fromDrop) {
+        if (fromDrop || filesOnly) {
           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 vBounds = viewport?.currentView
     if (vBounds) {
@@ -47,7 +47,7 @@ export class PreviewManager {
     commonBounds = BoundsUtils.expandBounds(commonBounds, SVG_EXPORT_PADDING)
 
     // 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] => {
       return [(p[0] - commonBounds.minX).toFixed(2), (p[1] - commonBounds.minY).toFixed(2)]
@@ -123,8 +123,8 @@ export class PreviewManager {
     return svgElement
   }
 
-  exportAsSVG() {
-    const svgElement = this.generatePreviewJsx()
+  exportAsSVG(ratio: number) {
+    const svgElement = this.generatePreviewJsx(undefined, ratio)
     return svgElement ? ReactDOMServer.renderToString(svgElement) : ''
   }
 }
@@ -134,12 +134,12 @@ export class PreviewManager {
  *
  * @param serializedApp
  */
-export function generateSVGFromApp(serializedApp: TLDocumentModel<Shape>) {
+export function generateSVGFromModel(serializedApp: TLDocumentModel<Shape>, ratio = 4 / 3) {
   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)
-  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 */
-import * as React from 'react'
+import { TLAsset, TLImageShape, TLImageShapeProps } from '@tldraw/core'
 import { HTMLContainer, TLComponentProps } from '@tldraw/react'
-import { isSafari, TLAsset, TLImageShape, TLImageShapeProps } from '@tldraw/core'
 import { observer } from 'mobx-react-lite'
+import * as React from 'react'
 import { LogseqContext } from '../logseq-context'
 import { BindingIndicator } from './BindingIndicator'
 
@@ -80,10 +80,6 @@ export class ImageShape extends TLImageShape<ImageShapeProps> {
   })
 
   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
     const bounds = this.getBounds()
     const {
@@ -94,6 +90,7 @@ export class ImageShape extends TLImageShape<ImageShapeProps> {
     const asset = assets.find(ass => ass.id === assetId)
 
     if (asset) {
+      // TODO: add clipping
       const [t, r, b, l] = Array.isArray(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
 
       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 {
       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 */
-import { isSafari, TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
+import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
 import { HTMLContainer, TLComponentProps } from '@tldraw/react'
 import { action, computed } from 'mobx'
 import { observer } from 'mobx-react-lite'
@@ -131,10 +131,6 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
   }
 
   getShapeSVGJsx() {
-    if (isSafari()) {
-      // Safari doesn't support foreignObject well
-      return super.getShapeSVGJsx(null);
-    }
     // Do not need to consider the original point here
     const bounds = this.getBounds()
     const embedId = this.embedId
@@ -142,35 +138,28 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
     if (embedId) {
       return (
         <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>
       )
     }

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

@@ -679,9 +679,10 @@ button.tl-select-input-trigger {
   opacity: 0.5;
 }
 
+/* This will breaks block selection
 [data-inner-events=false] * {
   user-select: none;
-}
+} */
 
 .tl-logseq-portal-container {
   @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 = {
   plugins: {
     'postcss-import': {},
@@ -5,6 +7,7 @@ module.exports = {
     'postcss-import-ext-glob': {},
     'tailwindcss/nesting': {},
     tailwindcss: {
+      ...rootTailwindConfig,
       content: ['./**/*.jsx', '../apps/**/*.{js,jsx,ts,tsx}'],
     },
     autoprefixer: {},

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

@@ -1,7 +1,7 @@
 import { uniqueId, fileToBase64 } from '@tldraw/core'
 import React from 'react'
 import ReactDOM from 'react-dom'
-import { App as TldrawApp } from '@tldraw/logseq'
+import { App as TldrawApp, generateJSXFromModel } from '@tldraw/logseq'
 
 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)
   React.useEffect(() => {
     if (anchor) {
       return
     }
-    let el = document.querySelector('#theme-switcher')
+    const id = 'status-bar-switcher-' + uniqueId()
+    let el = document.getElementById(id)
     if (!el) {
       el = document.createElement('div')
-      el.id = 'theme-switcher'
+      el.id = id
       let timer = setInterval(() => {
         const statusBarAnchor = document.querySelector('#tl-statusbar-anchor')
         if (statusBarAnchor) {
@@ -104,26 +105,76 @@ const ThemeSwitcher = ({ theme, setTheme }) => {
     }
   })
 
-  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"
+      className="flex items-center justify-center bg-grey border px-1"
       style={{ fontSize: '1em' }}
-      onClick={() => setTheme(t => (t === 'dark' ? 'light' : 'dark'))}
+      onClick={onClick}
     >
-      {theme} theme
+      {label}
     </button>,
     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 => {
   return Promise.resolve({
     pages: ['foo', 'bar', 'asdf'].filter(p => p.includes(q)),
@@ -136,8 +187,6 @@ const searchHandler = q => {
 }
 
 export default function App() {
-  const [theme, setTheme] = React.useState('light')
-
   const [model, setModel] = React.useState(documentModel)
 
   // Mimic external reload event
@@ -153,7 +202,8 @@ export default function App() {
 
   return (
     <div className={`h-screen w-screen`}>
-      <ThemeSwitcher theme={theme} setTheme={setTheme} />
+      <ThemeSwitcher />
+      <PreviewButton model={model} />
       <TldrawApp
         renderers={{
           Page,
@@ -170,7 +220,10 @@ export default function App() {
           makeAssetUrl: a => a,
         }}
         model={model}
-        onPersist={onPersist}
+        onPersist={app => {
+          onPersist(app)
+          setModel(app.serialized)
+        }}
       />
     </div>
   )

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

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

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

@@ -1,7 +1,7 @@
 import { Color } from '../types'
 
 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'})`
   }
 

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

@@ -41,6 +41,7 @@
     "mobx": "^6.7.0",
     "mobx-react-lite": "^3.4.0",
     "mousetrap": "^1.6.5",
+    "polished": "^4.2.2",
     "rbush": "^3.0.1",
     "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 { useRendererContext } from '../../../hooks'
 import type { TLGridProps } from '../../../types'
@@ -10,7 +10,7 @@ const STEPS = [
   [0.7, 2.5, 1],
 ]
 
-export const Grid = observer(function Grid({ size }: TLGridProps) {
+const SVGGrid = observer(function CanvasGrid({ size }: TLGridProps) {
   const {
     viewport: {
       camera: { point, zoom },
@@ -25,7 +25,9 @@ export const Grid = observer(function Grid({ size }: TLGridProps) {
           const yo = point[1] * zoom
           const gxo = xo > 0 ? xo % s : s + (xo % 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 (
             <pattern
@@ -35,7 +37,9 @@ export const Grid = observer(function Grid({ size }: TLGridProps) {
               height={s}
               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>
           )
         })}
@@ -46,3 +50,7 @@ export const Grid = observer(function Grid({ size }: TLGridProps) {
     </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 rDelta = React.useRef<number[]>([0, 0])
+  const rWheelTs = React.useRef<number>(0)
 
   const events = React.useMemo(() => {
     const onWheel: Handler<'wheel', WheelEvent> = gesture => {
@@ -23,10 +24,12 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
 
       const [x, y, z] = normalizeWheel(event)
 
-      if (inputs.state === 'pinching') {
+      if (inputs.state === 'pinching' || rWheelTs.current >= event.timeStamp) {
         return
       }
 
+      rWheelTs.current = event.timeStamp
+
       if ((event.altKey || event.ctrlKey || event.metaKey) && event.buttons === 0) {
         const bounds = viewport.bounds
         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);
   }
 
+  .tl-grid-canvas {
+    position: absolute;
+    touch-action: none;
+    pointer-events: none;
+    user-select: none;
+  }
+
   .tl-grid {
     position: absolute;
     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"
   integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
 
-polished@^4.0.0:
+polished@^4.0.0, polished@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
   integrity sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==