Browse Source

Merge branch 'master' into enhance/properties

Gabriel Horner 3 năm trước cách đây
mục cha
commit
3a83bce501
93 tập tin đã thay đổi với 577 bổ sung377 xóa
  1. 3 0
      .gitignore
  2. 5 0
      deps.edn
  3. 3 2
      docs/develop-logseq.md
  4. 19 0
      e2e-tests/basic.spec.ts
  5. 37 0
      e2e-tests/editor.spec.ts
  6. 1 1
      e2e-tests/whiteboards.spec.ts
  7. 1 0
      package.json
  8. 1 0
      postcss.config.js
  9. 0 4
      resources/css/common.css
  10. 0 0
      resources/whiteboard/onboarding.edn
  11. 28 0
      src/bench/frontend/benchmark_test_runner.cljs
  12. 8 0
      src/bench/frontend/macros.cljc
  13. 1 1
      src/main/frontend/components/block.cljs
  14. 1 1
      src/main/frontend/components/datetime.cljs
  15. 2 1
      src/main/frontend/components/page_menu.cljs
  16. 1 1
      src/main/frontend/components/right_sidebar.cljs
  17. 4 0
      src/main/frontend/components/search.css
  18. 3 2
      src/main/frontend/components/settings.cljs
  19. 8 4
      src/main/frontend/components/sidebar.cljs
  20. 10 0
      src/main/frontend/components/whiteboard.css
  21. 2 3
      src/main/frontend/config.cljs
  22. 27 25
      src/main/frontend/dicts.cljc
  23. 2 0
      src/main/frontend/extensions/tldraw.cljs
  24. 4 11
      src/main/frontend/handler/editor.cljs
  25. 2 1
      src/main/frontend/mobile/footer.cljs
  26. 12 9
      src/main/frontend/modules/file/core.cljs
  27. 18 0
      src/main/frontend/modules/file/uprint.cljs
  28. 2 1
      src/main/frontend/modules/outliner/transaction.cljc
  29. 9 6
      src/main/frontend/state.cljs
  30. 3 3
      src/main/frontend/ui.css
  31. 9 1
      tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx
  32. 3 3
      tldraw/apps/tldraw-logseq/src/components/Devtools/Devtools.tsx
  33. 1 4
      tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx
  34. 2 3
      tldraw/apps/tldraw-logseq/src/components/inputs/ColorInput.tsx
  35. 1 1
      tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts
  36. 1 0
      tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts
  37. 23 17
      tldraw/apps/tldraw-logseq/src/lib/shapes/ImageShape.tsx
  38. 38 18
      tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx
  39. 1 1
      tldraw/apps/tldraw-logseq/src/lib/shapes/PencilShape.tsx
  40. 6 0
      tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx
  41. 12 3
      tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx
  42. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/LogseqPortalTool.tsx
  43. 11 5
      tldraw/apps/tldraw-logseq/src/styles.css
  44. 3 4
      tldraw/demo/src/App.jsx
  45. 2 2
      tldraw/demo/vite.config.js
  46. 1 7
      tldraw/packages/core/src/lib/TLApi/TLApi.ts
  47. 2 13
      tldraw/packages/core/src/lib/TLApp/TLApp.ts
  48. 0 7
      tldraw/packages/core/src/lib/TLBaseLineBindingState.ts
  49. 8 18
      tldraw/packages/core/src/lib/TLInputs.ts
  50. 0 2
      tldraw/packages/core/src/lib/TLSettings.ts
  51. 0 13
      tldraw/packages/core/src/lib/TLState.ts
  52. 14 12
      tldraw/packages/core/src/lib/TLViewport.ts
  53. 1 1
      tldraw/packages/core/src/lib/shapes/TLShape/TLShape.tsx
  54. 0 7
      tldraw/packages/core/src/lib/tools/TLBoxTool/states/CreatingState.tsx
  55. 0 7
      tldraw/packages/core/src/lib/tools/TLDotTool/states/CreatingState.tsx
  56. 7 3
      tldraw/packages/core/src/lib/tools/TLDrawTool/TLDrawTool.tsx
  57. 5 5
      tldraw/packages/core/src/lib/tools/TLDrawTool/states/CreatingState.tsx
  58. 3 4
      tldraw/packages/core/src/lib/tools/TLDrawTool/states/IdleState.tsx
  59. 41 0
      tldraw/packages/core/src/lib/tools/TLDrawTool/states/PinchingState.ts
  60. 1 0
      tldraw/packages/core/src/lib/tools/TLDrawTool/states/index.ts
  61. 0 4
      tldraw/packages/core/src/lib/tools/TLEraseTool/states/ErasingState.tsx
  62. 5 1
      tldraw/packages/core/src/lib/tools/TLMoveTool/TLMoveTool.ts
  63. 0 4
      tldraw/packages/core/src/lib/tools/TLMoveTool/states/IdleState.tsx
  64. 1 1
      tldraw/packages/core/src/lib/tools/TLMoveTool/states/PanningState.tsx
  65. 2 2
      tldraw/packages/core/src/lib/tools/TLMoveTool/states/PinchingState.ts
  66. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/BrushingState.ts
  67. 2 31
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PinchingState.ts
  68. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingBoundsBackgroundState.ts
  69. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingCanvasState.ts
  70. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingHandleState.ts
  71. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingMinimapState.ts
  72. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingResizeHandleState.ts
  73. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingRotateHandleState.ts
  74. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingSelectedShapeState.ts
  75. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingShapeBehindBoundsState.ts
  76. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingShapeState.ts
  77. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/ResizingState.ts
  78. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/RotatingState.ts
  79. 0 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/TranslatingState.ts
  80. 0 1
      tldraw/packages/core/src/types/TLEventHandlers.ts
  81. 0 1
      tldraw/packages/core/src/types/TLEventMap.ts
  82. 2 3
      tldraw/packages/core/src/types/TLEvents.ts
  83. 8 0
      tldraw/packages/core/src/utils/index.ts
  84. 6 0
      tldraw/packages/react/src/hooks/useCanvasEvents.ts
  85. 3 5
      tldraw/packages/react/src/hooks/useDebounced.ts
  86. 82 30
      tldraw/packages/react/src/hooks/useGestureEvents.ts
  87. 3 0
      tldraw/packages/react/src/hooks/usePreventNavigation.ts
  88. 0 1
      tldraw/packages/react/src/hooks/useSetup.ts
  89. 3 0
      tldraw/packages/react/src/hooks/useStylesheet.ts
  90. 1 1
      tldraw/packages/react/src/hooks/useZoom.ts
  91. 0 1
      tldraw/packages/react/src/types/TLReactCustomEvents.ts
  92. 0 1
      tldraw/packages/react/src/types/TLReactEventHandlers.ts
  93. 45 0
      yarn.lock

+ 3 - 0
.gitignore

@@ -14,6 +14,7 @@ pom.xml.asc
 node_modules/
 static/
 tmp
+cljs-test-runner-out
 
 .cpcache/
 /src/gen
@@ -49,3 +50,5 @@ startup.png
 
 ios/App/App/capacitor.config.json
 android/app/src/main/assets/capacitor.config.json
+
+*.sublime-*

+ 5 - 0
deps.edn

@@ -47,6 +47,11 @@
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
+           :bench {:extra-paths ["src/bench/"]
+                   :extra-deps {olical/cljs-test-runner {:mvn/version "3.8.0"}
+                                fipp/fipp {:mvn/version "0.6.26"}}
+                   :main-opts ["-m" "cljs-test-runner.main" "-d" "src/bench" "-n" "frontend.benchmark-test-runner"]}
+
            ;; Use :replace-deps for tools. See https://github.com/clj-kondo/clj-kondo/issues/1536#issuecomment-1013006889
            :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.10.14"}}
                        :main-opts  ["-m" "clj-kondo.main"]}}}

+ 3 - 2
docs/develop-logseq.md

@@ -30,7 +30,7 @@ Then open the browser <http://localhost:3001>.
 yarn release
 ```
 
-The released files will be at `resources/` directory.
+The released files will be at `static/` directory.
 
 ## Desktop app development
 
@@ -55,7 +55,8 @@ yarn dev-electron-app
 Alternatively, run `bb dev:electron-start` to do this step with one command. To
 download bb, see https://github.com/babashka/babashka#installation.
 
-3. (Optional) Update dependencies if your are updating from an old branch
+3. (Optional) Update dependencies if `resources/package.json` has changed since
+the last time you used dev Logseq.
 
 ```bash
 # pull new changes

+ 19 - 0
e2e-tests/basic.spec.ts

@@ -228,3 +228,22 @@ test('invalid page props #3944', async ({ page, block }) => {
   // Force rendering property block
   await block.enterNext()
 })
+
+test('Scheduled date picker should point to the already specified Date #6985', async({page,block})=>{
+  await createRandomPage(page)
+
+  await block.mustFill('testTask \n SCHEDULED: <2000-05-06 Sat>')
+  await block.enterNext()
+  await page.waitForTimeout(500)
+  await block.escapeEditing()
+
+  // Open date picker
+  await page.click('a.opacity-80')
+  await page.waitForTimeout(500)
+  expect(page.locator('text=May 2000')).toBeVisible()
+  expect(page.locator('td:has-text("6").active')).toBeVisible()
+
+  // Close date picker
+  await page.click('a.opacity-80')
+  await page.waitForTimeout(500)
+})

+ 37 - 0
e2e-tests/editor.spec.ts

@@ -537,3 +537,40 @@ test('should show text after soft return when node is collapsed #5074', async ({
     'Before soft return\nAfter soft return'
   )
 })
+
+test('should not erase typed text when expanding block quickly after typing #3891', async ({ page, block }) => {
+  await createRandomPage(page)
+
+  await block.mustFill('initial text,')
+  await page.waitForTimeout(500)
+  await page.type('textarea >> nth=0', ' then expand', { delay: 10 })
+  // A quick cmd-down must not destroy the typed text
+  if (IsMac) {
+    await page.keyboard.press('Meta+ArrowDown')
+  } else {
+    await page.keyboard.press('Control+ArrowDown')
+  }
+  await page.waitForTimeout(500)
+  expect(await page.inputValue('textarea >> nth=0')).toBe(
+    'initial text, then expand'
+  )
+
+  // First undo should delete the last typed information, not undo a no-op expand action
+  if (IsMac) {
+    await page.keyboard.press('Meta+z')
+  } else {
+    await page.keyboard.press('Control+z')
+  }
+  expect(await page.inputValue('textarea >> nth=0')).toBe(
+    'initial text,'
+  )
+
+  if (IsMac) {
+    await page.keyboard.press('Meta+z')
+  } else {
+    await page.keyboard.press('Control+z')
+  }
+  expect(await page.inputValue('textarea >> nth=0')).toBe(
+    ''
+  )
+})

+ 1 - 1
e2e-tests/whiteboards.spec.ts

@@ -19,7 +19,7 @@ test('enable whiteboards', async ({ page }) => {
 test('create new whiteboard', async ({ page }) => {
     await page.click('.nav-header .whiteboard')
     await page.click('#tl-create-whiteboard')
-    await expect(page.locator('.logseq-tldraw')).toHaveCount(1)
+    await expect(page.locator('.logseq-tldraw')).toBeVisible()
 })
 
 test('check if the page contains the onboarding whiteboard', async ({ page }) => {

+ 1 - 0
package.json

@@ -12,6 +12,7 @@
         "@tailwindcss/line-clamp": "0.4.2",
         "@tailwindcss/typography": "0.5.7",
         "@types/gulp": "^4.0.7",
+        "autoprefixer": "^10.4.13",
         "cross-env": "^7.0.3",
         "cssnano": "^5.1.13",
         "del": "^6.0.0",

+ 1 - 0
postcss.config.js

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

+ 0 - 4
resources/css/common.css

@@ -86,12 +86,10 @@ html[data-theme='dark'] {
   --ls-scrollbar-foreground-color: #11505f;
   --ls-scrollbar-background-color: rgba(30, 60, 67, 0.1);
   --ls-scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.2);
-  --ls-head-text-color: var(--ls-link-text-color);
   --ls-cloze-text-color: #8fbc8f;
   --ls-icon-color: var(--ls-link-text-color);
   --ls-search-icon-color: var(--ls-link-text-color);
   --ls-a-chosen-bg: var(--ls-secondary-background-color);
-  --ls-right-sidebar-code-bg-color: #04303c;
   --ls-pie-bg-color: #01303b;
   --ls-pie-fg-color: #0b5869;
   --ls-highlight-color-gray: var(--color-gray-900);
@@ -165,12 +163,10 @@ html[data-theme='light'] {
   --ls-scrollbar-foreground-color: rgba(0, 0, 0, 0.1);
   --ls-scrollbar-background-color: rgba(0, 0, 0, 0.05);
   --ls-scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.2);
-  --ls-head-text-color: var(--ls-link-text-color);
   --ls-cloze-text-color: #0000cd;
   --ls-icon-color: #646464;
   --ls-search-icon-color: var(--ls-icon-color);
   --ls-a-chosen-bg: #f7f7f7;
-  --ls-right-sidebar-code-bg-color: var(--ls-secondary-background-color);
   --ls-pie-bg-color: #e1e1e1;
   --ls-pie-fg-color: #0a4a5d;
   --ls-highlight-color-gray: var(--color-gray-100);

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
resources/whiteboard/onboarding.edn


+ 28 - 0
src/bench/frontend/benchmark_test_runner.cljs

@@ -0,0 +1,28 @@
+(ns frontend.benchmark-test-runner
+  "Runs a benchmark"
+  (:require [clojure.edn :as edn]
+            [frontend.macros :refer [slurped]]
+            [frontend.modules.file.uprint :as up]
+            [clojure.pprint :as pprint]
+            [clojure.test :refer [deftest testing]]
+            [fipp.edn :as fipp]))
+
+(def onboarding
+  (edn/read-string (slurped "resources/whiteboard/onboarding.edn")))
+
+(deftest test-pp-str
+  (testing "pp-str benchmark"
+    (simple-benchmark []
+                      (with-out-str (pprint/pprint onboarding))
+                      10)
+    (simple-benchmark []
+                      (with-out-str (fipp/pprint onboarding))
+                      10)
+    (simple-benchmark []
+                      (up/ugly-pr-str onboarding)
+                      10)
+    (simple-benchmark []
+                      (pr-str onboarding)
+                      10)
+    ;; uncomment to see the output
+    #_(println (up/ugly-pr-str onboarding))))

+ 8 - 0
src/bench/frontend/macros.cljc

@@ -0,0 +1,8 @@
+(ns frontend.macros
+  #?(:cljs (:require-macros [frontend.macros])))
+
+#?(:clj
+   (defmacro slurped
+     "Like slurp, but at compile time"
+     [filename]
+     (slurp filename)))

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

@@ -2069,7 +2069,7 @@
         (do
           (util/stop e)
           (state/conj-selection-block! (gdom/getElement block-id) :down)
-          (when (and block-id (not (state/get-selection-start-block)))
+          (when block-id
             (state/set-selection-start-block! block-id)))
         (when (contains? #{1 0} button)
           (when-not (target-forbidden-edit? target)

+ 1 - 1
src/main/frontend/components/datetime.cljs

@@ -136,7 +136,7 @@
                (reset! *timestamp {:time ""
                                    :repeater {}}))
              (when-not (:date-picker/date @state/state)
-               (state/set-state! :date-picker/date (t/today))))
+               (state/set-state! :date-picker/date (get ts :date (t/today)))))
            state)
    :will-unmount (fn [state]
                    (clear-timestamp!)

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

@@ -63,7 +63,8 @@
           repo (state/sub :git/current-repo)
           page (db/entity repo [:block/name page-name])
           page-original-name (:block/original-name page)
-          block? (and page (util/uuid-string? page-name))
+          whiteboard? (= "whiteboard" (:block/type page))
+          block? (and page (util/uuid-string? page-name) (not whiteboard?))
           contents? (= page-name "contents")
           properties (:block/properties page)
           public? (true? (:public properties))

+ 1 - 1
src/main/frontend/components/right_sidebar.cljs

@@ -104,7 +104,7 @@
 
     :page-presentation
     (let [page-name (:block/name (db/entity db-id))]
-      [[:a {:href (rfe/href :page {:name page-name})}
+      [[:a.page-title {:href (rfe/href :page {:name page-name})}
         (db-model/get-page-original-name page-name)]
        [:div.ml-2.slide.mt-2
         (slide/slide page-name)]])

+ 4 - 0
src/main/frontend/components/search.css

@@ -2,6 +2,10 @@
   > .inner {
     width: 100%;
   }
+
+  .search-result {
+    @apply flex;
+  }
 }
 
 .search-ac {

+ 3 - 2
src/main/frontend/components/settings.cljs

@@ -6,6 +6,7 @@
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.storage :as storage]
+            [frontend.spec.storage :as storage-spec]
             [frontend.date :as date]
             [frontend.dicts :as dicts]
             [frontend.handler :as handler]
@@ -490,7 +491,7 @@
         [on? set-on?] (rum/use-state value)
         on-toggle #(let [v (not on?)]
                      (set-on? v)
-                     (storage/set :lsp-core-enabled v))]
+                     (storage/set ::storage-spec/lsp-core-enabled v))]
     [:div.flex.items-center
      (ui/toggle on? on-toggle true)
      (when (not= (boolean value) on?)
@@ -676,7 +677,7 @@
             :on-key-press  (fn [e]
                              (when (= "Enter" (util/ekey e))
                                (update-home-page e)))}]]]])
-     (when (and (util/electron?) config/enable-plugins?) (plugin-system-switcher-row))
+     (when (and (util/electron?) config/feature-plugin-system-on?) (plugin-system-switcher-row))
      (flashcards-switcher-row enable-flashcards?)
      (zotero-settings-row)
      (when-not web-platform?

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

@@ -536,10 +536,14 @@
 (defn- hide-context-menu-and-clear-selection
   [e]
   (state/hide-custom-context-menu!)
-  (when-not (or (gobj/get e "shiftKey")
-                (util/meta-key? e)
-                (state/get-edit-input-id))
-    (editor-handler/clear-selection!)))
+  (let [block (.closest (.-target e) ".ls-block")]
+    (when-not (or (gobj/get e "shiftKey")
+                  (util/meta-key? e)
+                  (state/get-edit-input-id)
+                  (and block
+                       (or (= block (.-target e))
+                           (.contains block (.-target e)))))
+      (editor-handler/clear-selection!))))
 
 (rum/defc render-custom-context-menu
   [links position]

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

@@ -210,3 +210,13 @@ input.tl-text-input {
     right: 0;
   }
 }
+
+/* disable user select globally for whiteboard on iOS/iPad. Is there a better option? */
+html:is(.is-ios, is-native-ios, is-native-ipad) [data-page="whiteboard"] * {
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}

+ 2 - 3
src/main/frontend/config.cljs

@@ -39,9 +39,7 @@
 ;; =============
 
 (goog-define ENABLE-PLUGINS true)
-(defonce enable-plugins? ENABLE-PLUGINS)
-
-(swap! state/state assoc :plugin/enabled enable-plugins?)
+(defonce feature-plugin-system-on? ENABLE-PLUGINS)
 
 ;; Desktop only as other platforms requires better understanding of their
 ;; multi-graph workflows and optimal place for a "global" dir
@@ -50,6 +48,7 @@
 ;; User level configuration for whether plugins are enabled
 (defonce lsp-enabled?
          (and (util/electron?)
+              (not (false? feature-plugin-system-on?))
               (state/lsp-enabled?-or-theme)))
 
 (defn plugin-config-enabled?

+ 27 - 25
src/main/frontend/dicts.cljc

@@ -1136,6 +1136,7 @@
         :content/open-in-sidebar "Ouvrir dans la barre latérale"
         :content/copy-as-json "Copier au format JSON"
         :content/click-to-edit "Cliquer pour éditer"
+        :settings-page/custom-date-format-warning "Réindexation requise ! Les réfeŕences vers les journaux existantes risquent d'être cassées !"
         :settings-page/edit-config-edn "Editer config.edn (pour le repo actuel)"
         :settings-page/preferred-file-format "Format de fichier préféré"
         :settings-page/preferred-workflow "Workflow préféré"
@@ -1144,6 +1145,7 @@
         :settings-page/enable-developer-mode "Activer le mode développeur"
         :settings-page/disable-developer-mode "Désactiver le mode développeur"
         :settings-page/developer-mode-desc "Le mode développeur aide les contributeurs et les développeurs d'extension à tester leur intégration avec Logseq."
+        :settings-page/preferred-pasting-file "Fichier de préférence pour le collage"
         :logseq "Logseq"
         :on "ON"
         :more-options "Plus d'options"
@@ -1186,9 +1188,9 @@
         :auto-heading "Titres automatiques"
         :cards-view "Voir les cartes"
         :close "Fermer"
-        :convert-markdown "Convertir les entêtes Markdown en une liste non-ordonnée (# ->..."
+        :convert-markdown "Convertir les entêtes Markdown en une liste non-ordonnée (# -> -)"
         :delete "Effacer"
-        :developer-mode-alert "Vous devez redémarrer l'application pour charger l'extension s..."
+        :developer-mode-alert "Vous devez redémarrer l'application pour charger l'extension système. Voulez-vous redémarrer maintenant ?"
         :discourse-title "Notre forum !"
         :export "Exporter"
         :export-datascript-edn "Exporter datascript EDN"
@@ -1220,15 +1222,15 @@
         :plugins "Extensions"
         :port "Port"
         :re-index-detail "Reconstruire le graphe"
-        :re-index-discard-unsaved-changes-warning "La réindexation va effacer le graphe actuel, puis ..."
-        :re-index-multiple-windows-warning "Vous devez d'abord fermer les autres fenêtres avant de réindexer..."
-        :relaunch-confirm-to-work "Il est nécessaire de relancer l'application pour que ça fonctionne. Voulez-vous..."
+        :re-index-discard-unsaved-changes-warning "La réindexation va effacer le graphe actuel, puis l'indéxer à nouveau. Ceci risque de vous faire perdre le travail non sauvegardé. Continuer ?"
+        :re-index-multiple-windows-warning "Vous devez d'abord fermer les autres fenêtres avant de réindexer"
+        :relaunch-confirm-to-work "Il est nécessaire de relancer l'application pour que ça fonctionne. Voulez-vous redémarrer ?"
         :remove-heading "Retirer les entêtes"
         :remove-orphaned-pages "Supprimer les pages orphelines"
         :save "Sauver"
         :settings-of-plugins "Extensions"
         :sponsor-us "Supportez-nous"
-        :sync-from-local-changes-detected "Rafraîchissement des modifications détectées ..."
+        :sync-from-local-changes-detected "Le rafraîchissement va mettre à jour le contenu de Logseq en fonction des modifications réalisées sur les fichiers locaux. Voulez-vous continuer ?"
         :sync-from-local-files "Rafraîchir"
         :sync-from-local-files-detail "Importer les changements depuis les fichiers locaux"
         :themes "Thèmes"
@@ -1258,31 +1260,31 @@
         :file-rn/apply-rename "Appliquer le renommage"
         :file-rn/close-panel "Fermer le panneau"
         :file-rn/confirm-proceed "Mettre à jour le format !"
-        :file-rn/filename-desc-1 "Ce réglage configure la manière dont une page est enregistrée dans un..."
-        :file-rn/filename-desc-2 "Certains caractères spéciaux comme \"/\" or \"?\" sont invalides pour un ..."
-        :file-rn/filename-desc-3 "Logseq remplace les caractères invalides par leur ..."
-        :file-rn/filename-desc-4 "Le séparateur de namespace \"/\" est également remplacé par \"_..."
-        :file-rn/format-deprecated "Vous utilisez un format obsolète. Conversion en cours..."
+        :file-rn/filename-desc-1 "Ce réglage configure la manière dont une page est enregistrée dans un fichier. Logseq enregistre une page dans uen fichier portant le même nom."
+        :file-rn/filename-desc-2 "Certains caractères spéciaux comme \"/\" or \"?\" sont invalides pour un nom de fichier."
+        :file-rn/filename-desc-3 "Logseq remplace les caractères invalides par leur équivalent encodé façon URL pour les rendre valides (ex: \"?\" devient \"%3F\")"
+        :file-rn/filename-desc-4 "Le séparateur de namespace \"/\" est également remplacé par \"___\" (triple underscore) pour des raisons esthétiques."
+        :file-rn/format-deprecated "Vous utilisez un format obsolète. Mettre à jour le format est fortement recommandé. Veuillez sauvegarder vos données, fermer Logseq sur vos autres postes avant de lancer l'opération."
         :file-rn/instruct-1 "Le changement de format de nom de fichier se fait en 2 étapes :"
         :file-rn/instruct-2 "1. Cliquez "
-        :file-rn/instruct-3 "2. Suivez les instructions ci-dessous pour renommer le fichier ..."
+        :file-rn/instruct-3 "2. Suivez les instructions ci-dessous pour renommer le fichier au nouveau format :"
         :file-rn/legend "🟢 Actions de renommage facultatives; 🟡 Action de renommage requise"
-        :file-rn/need-action "Les actions de renommage de fichier sont suggérées pour être compatibles avec le nouveau..."
+        :file-rn/need-action "Les actions de renommage de fichier sont suggérées pour être compatibles avec le nouveau format. Une réindexation est requise sur tous les postes quand les fichiers renommés auront été "
         :file-rn/no-action "Bravo ! Aucune autre action requise"
         :file-rn/optional-rename "Suggestion : "
         :file-rn/or-select-actions "ou renommez individuellement les fichiers suivants, puis "
-        :file-rn/or-select-actions-2 ". Ces actions ne sont pas disponibles dès lors que vous fermez ..."
+        :file-rn/or-select-actions-2 ". Ces actions ne sont pas disponibles dès lors que vous fermez ce panneau."
         :file-rn/otherwise-breaking "Ou le titre deviendra"
-        :file-rn/re-index "La réindexation est fortement recommandée après que les fichiers aient..."
+        :file-rn/re-index "La réindexation est fortement recommandée après que les fichiers aient été renommés, puis sur les autres postes après synchronisation."
         :file-rn/rename "renommer le fichier \"{1}\" en \"{2}\""
         :file-rn/rename-sm "Renommer"
         :file-rn/select-confirm-proceed "Dev: format d'écriture"
-        :file-rn/select-format "(Option du Mode Developpeur, Danger !) Sélectionnez le nom de fichier..."
+        :file-rn/select-format "(Option du Mode Developpeur, Danger !) Sélectionnez le format de nom de fichier"
         :file-rn/suggest-rename "Action requise : "
-        :file-rn/unreachable-title "Attention ! la page deviendra {1} sous..."
+        :file-rn/unreachable-title "Attention ! la page deviendra {1} sous le format actuel, à moins que vous n'ayez modifié la propriété `title::`"
         :graph/all-graphs "Tous les graphes"
         :graph/local-graphs "Locaux graphes"
-        :graph/persist "Logseq synchronise sont statut local, veuillez patienter pour..."
+        :graph/persist "Logseq synchronise son statut local, veuillez patienter quelques secondes."
         :graph/persist-error "La synchronisation interne a échoué."
         :graph/remote-graphs "Graphes distants"
         :graph/save "Enregistrement ..."
@@ -1303,12 +1305,12 @@
         :left-side-bar/new-whiteboard "Nouveau tableau blanc"
         :linked-references/filter-search "Rechercher dans les pages liées"
         :on-boarding/add-graph "Ajouter un graphe"
-        :on-boarding/demo-graph "Il s'agit d'un graphe de démo, les changements ne seront pas enregistrés ..."
-        :on-boarding/new-graph-desc-1 "Logseq supporte à la fois le Markdown et l'Org-mode. Vous pouvez..."
-        :on-boarding/new-graph-desc-2 "Après avoir ouvert votre dossier, cela créera..."
+        :on-boarding/demo-graph "Il s'agit d'un graphe de démo, les changements ne seront pas enregistrés à moins que vous n'ouvrir un dossier local."
+        :on-boarding/new-graph-desc-1 "Logseq supporte à la fois le Markdown et l'Org-mode. Vous pouvez ouvrir un dossier existant ou en créer un nouveau sur cet appareil. Vos données seront enregistrées uniquement sur cet appareil."
+        :on-boarding/new-graph-desc-2 "Après avoir ouvert votre dossier, cela créera 3 sous-dossiers :"
         :on-boarding/new-graph-desc-3 "/journals - contient vos pages du journal"
         :on-boarding/new-graph-desc-4 "/pages - contient les autres pages"
-        :on-boarding/new-graph-desc-5 "/logseq - contient la configuration, custom.css, et autres ..."
+        :on-boarding/new-graph-desc-5 "/logseq - contient la configuration, custom.css, et quelques métadonnées"
         :on-boarding/open-local-dir "Ouvre un dossier local"
         :page/action-publish "Publier"
         :page/add-to-favorites "Ajouter aux Favoris"
@@ -1338,8 +1340,8 @@
         :plugin/check-all-updates "Vérifier toutes les mises-à-jour"
         :plugin/check-update "Vérifier la mise à jour"
         :plugin/contribute "✨ Écrire et proposer une nouvelle extension"
-        :plugin/custom-js-alert "Fichier custom.js trouvé, il est autorisé à s'éxécuter..."
-        :plugin/delete-alert "Êtes-vous sûr de vouloir désintaller l'extension [{1}..."
+        :plugin/custom-js-alert "Fichier custom.js trouvé, est-il autorisé à s'éxécuter ? (Si vous ne comprenez pas le contenu de ce fichier, il est recommandé de ne pas en autoriser l'éxecution, car celà vous expose à des risques de sécurité)."
+        :plugin/delete-alert "Êtes-vous sûr de vouloir désintaller l'extension [{1}] ?"
         :plugin/disabled "Désactivée"
         :plugin/downloads "Téléchargements"
         :plugin/enabled "Activée"
@@ -1417,7 +1419,7 @@
         :settings-page/git-desc "est utilisé pour gérer les versions de pages, vous pouvez cliquer sur..."
         :settings-page/git-switcher-label "Activer les commits Git automatiques"
         :settings-page/home-default-page "Régler la page d'accueil par défaut"
-        :settings-page/login-prompt "Pour accéder aux nouvelles fonctionnalités avant tout le monde, vous devez..."
+        :settings-page/login-prompt "Pour accéder aux nouvelles fonctionnalités avant tout le monde, vous devez être sponsor ou \"backer\" (contributeur) sur Open Collective, puis vous connecter."
         :settings-page/network-proxy "Proxy réseau"
         :settings-page/plugin-system "Extensions"
         :settings-page/preferred-outdenting "Mise en retrait logique"

+ 2 - 0
src/main/frontend/extensions/tldraw.cljs

@@ -65,6 +65,8 @@
    :isWhiteboardPage model/whiteboard-page?
    :saveAsset save-asset-handler
    :makeAssetUrl editor-handler/make-asset-url
+   :addNewWhiteboard (fn [page-name]
+                       (whiteboard-handler/create-new-whiteboard-page! page-name))
    :addNewBlock (fn [content]
                   (str (whiteboard-handler/add-new-block! name content)))
    :sidebarAddBlock (fn [uuid type]

+ 4 - 11
src/main/frontend/handler/editor.cljs

@@ -3259,25 +3259,18 @@
         repo (state/get-current-repo)
         value (boolean value)]
     (when repo
-      (outliner-tx/transact!
+      (save-current-block!) ;; Save the input contents before collapsing 
+      (outliner-tx/transact! ;; Save the new collapsed state as an undo transaction (if it changed)
         {:outliner-op :collapse-expand-blocks}
         (doseq [block-id block-ids]
           (when-let [block (db/entity [:block/uuid block-id])]
-            (let [current-value (:block/collapsed? block)]
+            (let [current-value (boolean (:block/collapsed? block))]
               (when-not (= current-value value)
                 (let [block {:block/uuid block-id
                              :block/collapsed? value}]
                   (outliner-core/save-block! block)))))))
       (doseq [block-id block-ids]
-        (state/set-collapsed-block! block-id value))
-      (let [block-id (first block-ids)
-            input-pos (or (state/get-edit-pos) :max)]
-        ;; update editing input content
-        (when-let [editing-block (state/get-edit-block)]
-          (when (= (:block/uuid editing-block) block-id)
-            (edit-block! editing-block
-                         input-pos
-                         (state/get-edit-input-id))))))))
+        (state/set-collapsed-block! block-id value)))))
 
 (defn collapse-block! [block-id]
   (when (collapsable? block-id)

+ 2 - 1
src/main/frontend/mobile/footer.cljs

@@ -50,7 +50,8 @@
 
 (rum/defc footer < rum/reactive
   []
-  (when (and (not (state/sub :editor/editing?))
+  (when (and (#{:page :home} (state/sub [:route-match :data :name]))
+             (not (state/sub :editor/editing?))
              (state/sub :mobile/show-tabbar?)
              (state/get-current-repo))
     [:div.cp__footer.w-full.bottom-0.justify-between

+ 12 - 9
src/main/frontend/modules/file/core.cljs

@@ -4,6 +4,7 @@
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db.utils :as db-utils]
+            [frontend.modules.file.uprint :as up]
             [frontend.state :as state]
             [frontend.util.property :as property]
             [frontend.util.fs :as fs-util]
@@ -227,9 +228,11 @@
             new-content (cond
                           ;; TODO: treat as "edn" below too
                           (= "whiteboard" (:block/type page-block))
-                          (pr-str {:blocks tree
-                                   :pages (list (remove-transit-ids page-block))})
-
+                          (->
+                           (up/ugly-pr-str {:blocks tree
+                                            :pages (list (remove-transit-ids page-block))})
+                           (string/triml))
+                          
                           (= "edn" ext)
                           (let [{:keys [blocks refs]} (edn-transform-blocks page-block tree)]
                             (with-out-str
@@ -240,12 +243,12 @@
                                 :refs refs})))
 
                           :else
-                          (tree->file-content tree {:init-level init-level}))
-            files [[file-path new-content]]
-            repo (state/get-current-repo)]
-        (file-handler/alter-files-handler! repo files {} {}))
-      ;; In e2e tests, "card" page in db has no :file/path
-      (js/console.error "File path from page-block is not valid" page-block tree))))
+                                                                        (tree->file-content tree {:init-level init-level}))
+                                files [[file-path new-content]]
+                                repo (state/get-current-repo)]
+                            (file-handler/alter-files-handler! repo files {} {}))
+                          ;; In e2e tests, "card" page in db has no :file/path
+                          (js/console.error "File path from page-block is not valid" page-block tree))))
 
 (defn save-tree!
   [page-block tree]

+ 18 - 0
src/main/frontend/modules/file/uprint.cljs

@@ -0,0 +1,18 @@
+(ns frontend.modules.file.uprint
+  "A fast pprint alternative.")
+
+(defn print-prefix-map* [prefix m print-one writer opts]
+  (pr-sequential-writer
+    writer
+    (fn [e w opts]
+      (print-one (key e) w opts)
+      (-write w \space)
+      (print-one (val e) w opts))
+    (str prefix "\n{") \newline "}"
+    opts (seq m)))
+
+(defn ugly-pr-str
+  "Ugly printing fast, with newlines so that git diffs are smaller"
+  [x]
+  (with-redefs [print-prefix-map print-prefix-map*]
+    (pr-str x)))

+ 2 - 1
src/main/frontend/modules/outliner/transaction.cljc

@@ -4,6 +4,7 @@
 (defmacro transact!
   "Batch all the transactions in `body` to a single transaction, Support nested transact! calls.
   Currently there are no options, it'll execute body and collect all transaction data generated by body.
+  If no transactions are included in `body`, it does not save a transaction.
   `Args`:
     `opts`: Every key is optional, opts except `additional-tx` will be transacted as `tx-meta`.
             {:graph \"Which graph will be transacted to\"
@@ -32,7 +33,7 @@
                tx-meta# (first (map :tx-meta r#))
                all-tx# (concat tx# (:additional-tx opts#))
                opts## (merge (dissoc opts# :additional-tx) tx-meta#)]
-           (when (seq all-tx#)
+           (when (seq all-tx#) ;; If it's empty, do nothing
              (when-not (:nested-transaction? opts#) ; transact only for the whole transaction
                (let [result# (frontend.modules.outliner.datascript/transact! all-tx# opts##)]
                  {:tx-report result#

+ 9 - 6
src/main/frontend/state.cljs

@@ -9,6 +9,7 @@
             [electron.ipc :as ipc]
             [frontend.mobile.util :as mobile-util]
             [frontend.storage :as storage]
+            [frontend.spec.storage :as storage-spec]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
             [goog.dom :as gdom]
@@ -172,7 +173,7 @@
      ;; plugin
      :plugin/enabled                        (and (util/electron?)
                                                  ;; true false :theme-only
-                                                 ((fnil identity true) (storage/get :lsp-core-enabled)))
+                                                 ((fnil identity true) (storage/get ::storage-spec/lsp-core-enabled)))
      :plugin/preferences                    nil
      :plugin/indicator-text                 nil
      :plugin/installed-plugins              {}
@@ -921,14 +922,15 @@ Similar to re-frame subscriptions"
   (when-let [input (get-input)]
     (util/get-selection-start input)))
 
-(defn set-selection-start-block!
-  [start-block]
-  (swap! state assoc :selection/start-block start-block))
-
 (defn get-selection-start-block
   []
   (get @state :selection/start-block))
 
+(defn set-selection-start-block!
+  [start-block]
+  (when-not (get-selection-start-block)
+    (swap! state assoc :selection/start-block start-block)))
+
 (defn set-selection-blocks!
   ([blocks]
    (set-selection-blocks! blocks :down))
@@ -949,7 +951,8 @@ Similar to re-frame subscriptions"
   (swap! state assoc
          :selection/mode false
          :selection/blocks nil
-         :selection/direction :down))
+         :selection/direction :down
+         :selection/start-block nil))
 
 (defn get-selection-blocks
   []

+ 3 - 3
src/main/frontend/ui.css

@@ -359,10 +359,10 @@ html.is-mobile {
 }
 
 .type-icon {
-  @apply text-xs text-center flex items-center justify-center rounded border mr-2 relative;
+  @apply text-base text-center flex items-center justify-center rounded border mr-2 relative;
 
-  width: 1.5rem;
-  height: 1.5rem;
+  width: 24px;
+  height: 24px;
   flex-shrink: 0;
   border-color: var(--ls-primary-background-color);
   overflow: hidden;

+ 9 - 1
tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx

@@ -34,7 +34,15 @@ export const ContextMenu = observer(function ContextMenu({
   }, [])
 
   return (
-    <ReactContextMenu.Root>
+    <ReactContextMenu.Root
+      onOpenChange={open => {
+        if (open && !app.isIn('select.contextMenu')) {
+          app.transition('select').selectedTool.transition('contextMenu')
+        } else if (!open && app.isIn('select.contextMenu')) {
+          app.selectedTool.transition('idle')
+        }
+      }}
+    >
       <ReactContextMenu.Trigger>{children}</ReactContextMenu.Trigger>
       <ReactContextMenu.Content
         className="tl-menu tl-context-menu"

+ 3 - 3
tldraw/apps/tldraw-logseq/src/components/Devtools/Devtools.tsx

@@ -6,7 +6,7 @@ import ReactDOM from 'react-dom'
 import type { Shape } from '../../lib'
 
 const printPoint = (point: number[]) => {
-  return `[${point.map(d => d.toFixed(2)).join(', ')}]`
+  return `[${point.map(d => d?.toFixed(2) ?? '-').join(', ')}]`
 }
 
 const HistoryStack = observer(function HistoryStack() {
@@ -25,7 +25,7 @@ const HistoryStack = observer(function HistoryStack() {
   }, [])
 
   React.useEffect(() => {
-    requestIdleCallback(() => {
+    requestAnimationFrame(() => {
       anchorRef.current
         ?.querySelector(`[data-item-index="${app.history.pointer}"]`)
         ?.scrollIntoView()
@@ -75,7 +75,7 @@ export const DevTools = observer(() => {
   }, [])
 
   const rendererStatusText = [
-    ['Z', zoom.toFixed(2)],
+    ['Z', zoom?.toFixed(2) ?? 'null'],
     ['MP', printPoint(inputs.currentPoint)],
     ['MS', printPoint(inputs.currentScreenPoint)],
     ['VP', printPoint(point)],

+ 1 - 4
tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx

@@ -95,10 +95,7 @@ export const PrimaryTools = observer(function PrimaryTools() {
 
   return (
     <div className="tl-primary-tools">
-      <div
-        className="tl-toolbar tl-tools-floating-panel"
-        data-tool-locked={app.settings.isToolLocked}
-      >
+      <div className="tl-toolbar tl-tools-floating-panel">
         <ToolButton title="Select" id="select" icon="select-cursor" />
         <ToolButton
           title="Move"

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

@@ -35,9 +35,7 @@ export function ColorInput({
 
   return (
     <Popover.Root>
-      <Popover.Trigger>
-        <button className="tl-color-drip mx-1">{renderColor(color)}</button>
-      </Popover.Trigger>
+      <Popover.Trigger className="tl-color-drip mx-1">{renderColor(color)}</Popover.Trigger>
 
       <Popover.Content
         className="tl-popover-content"
@@ -48,6 +46,7 @@ export function ColorInput({
         <div className={'tl-color-palette'}>
           {Object.values(Color).map(value => (
             <button
+              key={value}
               className={`tl-color-drip m-1${value === color ? ' active' : ''}`}
               onClick={() => setColor(value)}
             >

+ 1 - 1
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -248,7 +248,7 @@ export function usePaste() {
       }
 
       function tryCreateClonedShapesFromJSON(rawText: string) {
-        const result = app.api.getClonedShapesFromTldrString(rawText, point)
+        const result = app.api.getClonedShapesFromTldrString(decodeURIComponent(rawText), point)
         if (result) {
           const { shapes, assets, bindings } = result
           assetsToClone.push(...assets)

+ 1 - 0
tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts

@@ -26,6 +26,7 @@ export interface LogseqContextValue {
       query: string,
       filters: { 'pages?': boolean; 'blocks?': boolean; 'files?': boolean }
     ) => Promise<SearchResult>
+    addNewWhiteboard: (pageName: string) => void
     addNewBlock: (content: string) => string // returns the new block uuid
     queryBlockByUUID: (uuid: string) => any
     isWhiteboardPage: (pageName: string) => boolean

+ 23 - 17
tldraw/apps/tldraw-logseq/src/lib/shapes/ImageShape.tsx

@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import * as React from 'react'
 import { HTMLContainer, TLComponentProps } from '@tldraw/react'
-import { TLAsset, TLImageShape, TLImageShapeProps } from '@tldraw/core'
+import { isSafari, TLAsset, TLImageShape, TLImageShapeProps } from '@tldraw/core'
 import { observer } from 'mobx-react-lite'
 import { LogseqContext } from '../logseq-context'
 import { BindingIndicator } from './BindingIndicator'
@@ -80,6 +80,10 @@ 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 {
@@ -97,22 +101,24 @@ export class ImageShape extends TLImageShape<ImageShapeProps> {
       const make_asset_url = window.logseq?.api?.make_asset_url
 
       return (
-        <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>
+          <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>
       )
     } else {
       return super.getShapeSVGJsx({})

+ 38 - 18
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -54,7 +54,7 @@ const LogseqTypeTag = ({
   type,
   active,
 }: {
-  type: 'B' | 'P' | 'BA' | 'WP' | 'BS' | 'PS'
+  type: 'B' | 'P' | 'BA' | 'PA' | 'WA' | 'WP' | 'BS' | 'PS'
   active?: boolean
 }) => {
   const nameMapping = {
@@ -62,6 +62,8 @@ const LogseqTypeTag = ({
     P: 'page',
     WP: 'whiteboard',
     BA: 'new-block',
+    PA: 'new-page',
+    WA: 'new-whiteboard',
     BS: 'block-search',
     PS: 'page-search',
   }
@@ -459,32 +461,49 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
             <LogseqTypeTag active type="BA" />
             {q.length > 0 ? (
               <>
-                <strong>New whiteboard block:</strong>
+                <strong>New block:</strong>
                 {q}
               </>
             ) : (
-              <strong>New whiteboard block</strong>
+              <strong>New block</strong>
             )}
           </div>
         ),
       })
 
-      // New page option when no exact match
+      // New page or whiteboard option when no exact match
       if (!searchResult?.pages?.some(p => p.toLowerCase() === q.toLowerCase()) && q) {
-        options.push({
-          actionIcon: 'circle-plus',
-          onChosen: () => {
-            finishCreating(q)
-            return true
+        options.push(
+          {
+            actionIcon: 'circle-plus',
+            onChosen: () => {
+              finishCreating(q)
+              return true
+            },
+            element: (
+              <div className="tl-quick-search-option-row">
+                <LogseqTypeTag active type="PA" />
+                <strong>New page:</strong>
+                {q}
+              </div>
+            ),
           },
-          element: (
-            <div className="tl-quick-search-option-row">
-              <LogseqTypeTag active type="P" />
-              <strong>New page:</strong>
-              {q}
-            </div>
-          ),
-        })
+          {
+            actionIcon: 'circle-plus',
+            onChosen: () => {
+              handlers?.addNewWhiteboard(q)
+              finishCreating(q)
+              return true
+            },
+            element: (
+              <div className="tl-quick-search-option-row">
+                <LogseqTypeTag active type="WA" />
+                <strong>New whiteboard:</strong>
+                {q}
+              </div>
+            ),
+          }
+        )
       }
 
       // search filters
@@ -658,7 +677,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
         </div>
         <div className="tl-quick-search-options" ref={optionsWrapperRef}>
           <Virtuoso
-            style={{ height: Math.min(Math.max(1, options.length), 12) * 36 }}
+            style={{ height: Math.min(Math.max(1, options.length), 12) * 40 }}
             totalCount={options.length}
             itemContent={index => {
               const { actionIcon, onChosen, element } = options[index]
@@ -859,6 +878,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
       >
         {isBinding && <BindingIndicator mode="html" strokeWidth={strokeWidth} size={size} />}
         <div
+          data-inner-events={!tlEventsEnabled}
           onWheelCapture={stop}
           onPointerDown={stop}
           onPointerUp={stop}

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

@@ -126,7 +126,7 @@ export class PencilShape extends TLDrawShape<PencilShapeProps> {
     } = this
     return (
       <path
-        pointerEvents="none"
+        pointerEvents="all"
         d={pointsPath}
         strokeWidth={strokeWidth / 2}
         strokeLinejoin="round"

+ 6 - 0
tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx

@@ -7,6 +7,7 @@ import {
   TLTextShape,
   TLTextShapeProps,
   getComputedColor,
+  isSafari,
 } from '@tldraw/core'
 import { HTMLContainer, TLComponentProps } from '@tldraw/react'
 import { action, computed } from 'mobx'
@@ -306,6 +307,10 @@ export class TextShape extends TLTextShape<TextShapeProps> {
   }
 
   getShapeSVGJsx() {
+    if (isSafari()) {
+      // Safari doesn't support foreignObject well
+      return super.getShapeSVGJsx(null);
+    }
     const {
       props: { text, stroke, fontSize, fontFamily },
     } = this
@@ -319,6 +324,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
             color: getComputedColor(stroke, 'text'),
             fontSize,
             fontFamily,
+            display: 'contents',
           }}
         >
           {text}

+ 12 - 3
tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx

@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
+import { isSafari, TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
 import { HTMLContainer, TLComponentProps } from '@tldraw/react'
 import { action, computed } from 'mobx'
 import { observer } from 'mobx-react-lite'
@@ -131,17 +131,26 @@ 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
 
     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%]"
             />
@@ -162,7 +171,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
               </svg>
             </div>
           </foreignObject>
-        </>
+        </g>
       )
     }
     return super.getShapeSVGJsx({})

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

@@ -16,6 +16,6 @@ export class LogseqPortalTool extends TLTool<
   Shape = LogseqPortalShape
 
   onPinch: TLEvents<Shape>['pinch'] = info => {
-    this.app.viewport.pinchCamera(info.point, [0, 0], info.offset[0])
+    this.app.viewport.pinchZoom(info.point, info.delta, info.delta[2])
   }
 }

+ 11 - 5
tldraw/apps/tldraw-logseq/src/styles.css

@@ -230,7 +230,7 @@ html[data-theme='light'] {
 }
 
 .tl-statusbar {
-  @apply absolute flex items-center w-full bottom-0;
+  @apply fixed flex items-center w-full bottom-0;
 
   font-family: monospace;
   font-size: 10px;
@@ -651,6 +651,7 @@ button.tl-select-input-trigger {
   padding: 8px 16px;
   cursor: pointer;
   gap: 0.5em;
+  user-select: none;
 
   &[data-focused='true'] {
     background-color: var(--ls-menu-hover-color, #f4f5f7);
@@ -658,7 +659,8 @@ button.tl-select-input-trigger {
 }
 
 .tl-quick-search-option-row {
-  display: flex;
+  @apply flex items-center;
+
   gap: 0.5em;
 
   .breadcrumb {
@@ -667,7 +669,7 @@ button.tl-select-input-trigger {
 }
 
 .tl-quick-search-option-placeholder {
-  width: 20px;
+  width: 24px;
   flex-shrink: 0;
 }
 
@@ -675,6 +677,10 @@ button.tl-select-input-trigger {
   opacity: 0.5;
 }
 
+[data-inner-events=false] * {
+  user-select: none;
+}
+
 .tl-logseq-portal-container {
   @apply flex flex-col rounded-lg absolute;
 
@@ -729,8 +735,8 @@ button.tl-select-input-trigger {
   @apply flex items-center justify-center rounded text-base;
 
   flex-shrink: 0;
-  width: 20px;
-  height: 20px;
+  width: 24px;
+  height: 24px;
   line-height: 1;
   color: #fff;
   background: rgba(0, 0, 0, 0.5);

+ 3 - 4
tldraw/demo/src/App.jsx

@@ -29,7 +29,7 @@ const documentModel = onLoad() ?? {
           type: 'logseq-portal',
           parentId: 'page1',
           point: [369.109375, 170.5546875],
-          size: [0, 0],
+          size: [240, 0],
           stroke: '',
           fill: '',
           strokeWidth: 2,
@@ -44,8 +44,7 @@ const documentModel = onLoad() ?? {
   ],
 }
 
-const Page = props => {
-  const [value, setValue] = React.useState(JSON.stringify(props, null, 2))
+const Page = () => {
   return (
     <div className="w-full font-mono page">
       The Circle components are a collection of standardized UI elements and patterns for building
@@ -57,7 +56,7 @@ const Page = props => {
   )
 }
 
-const Block = props => {
+const Block = () => {
   return (
     <div className="w-full font-mono single-block">
       The Circle components are a collection of standardized UI elements and patterns for building

+ 2 - 2
tldraw/demo/vite.config.js

@@ -20,12 +20,12 @@ export default defineConfig({
         plugins: [[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }]],
       },
     }),
-    basicSsl(),
+    // basicSsl(),
   ],
   server: {
     port: '3031',
     fs: { strict: false },
-    https: true,
+    // https: true,
   },
   resolve: {
     alias: [

+ 1 - 7
tldraw/packages/core/src/lib/TLApi/TLApi.ts

@@ -166,12 +166,6 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
     return this
   }
 
-  toggleToolLock = (): this => {
-    const { settings } = this.app
-    settings.update({ showGrid: !settings.isToolLocked })
-    return this
-  }
-
   save = () => {
     this.app.save()
     return this
@@ -286,7 +280,7 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
     const getWhiteboardsTldrFromText = (text: string) => {
       const innerText = text.match(/<whiteboard-tldr>(.*)<\/whiteboard-tldr>/)?.[1]
       if (innerText) {
-        return safeParseJson(decodeURIComponent(innerText))
+        return safeParseJson(innerText)
       }
     }
 

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

@@ -494,7 +494,7 @@ export class TLApp<
         // convey the bindings to maintain the new links after pasting
         bindings: toJS(this.currentPage.bindings),
       })
-      const tldrawString = `<whiteboard-tldr>${encodeURIComponent(jsonString)}</whiteboard-tldr>`
+      const tldrawString = encodeURIComponent(`<whiteboard-tldr>${jsonString}</whiteboard-tldr>`)
       // FIXME: use `writeClipboard` in frontend.utils
       navigator.clipboard.write([
         new ClipboardItem({
@@ -925,18 +925,7 @@ export class TLApp<
     this.selectedTool.transition('idleHold')
   }
 
-  readonly onTransition: TLStateEvents<S, K>['onTransition'] = () => {
-    this.settings.update({ isToolLocked: false })
-  }
-
-  readonly onWheel: TLEvents<S, K>['wheel'] = (info, e) => {
-    if (e.ctrlKey || e.metaKey || this.isIn('select.contextMenu')) {
-      return
-    }
-
-    this.viewport.panCamera(info.delta)
-    this.inputs.onWheel([...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], e)
-  }
+  readonly onTransition: TLStateEvents<S, K>['onTransition'] = () => {}
 
   readonly onPointerDown: TLEvents<S, K>['pointer'] = (info, e) => {
     // Pan canvas when holding middle click

+ 0 - 7
tldraw/packages/core/src/lib/TLBaseLineBindingState.ts

@@ -216,16 +216,9 @@ export class TLBaseLineBindingState<
     if (this.currentShape) {
       this.app.setSelectedShapes([this.currentShape])
     }
-    if (!this.app.settings.isToolLocked) {
-      this.app.transition('select')
-    }
     this.app.persist()
   }
 
-  onWheel: TLStateEvents<S, K>['onWheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onExit: TLStateEvents<S, K>['onExit'] = () => {
     this.app.clearBindingShape()
     this.app.history.resume()

+ 8 - 18
tldraw/packages/core/src/lib/TLInputs.ts

@@ -33,9 +33,10 @@ export class TLInputs<K extends TLEventMap> {
     Object.assign(this.containerOffset, containerOffset)
   }
 
-  @action private updateModifiers(
-    event: K['gesture'] | K['pointer'] | K['keyboard'] | K['wheel'] | K['touch']
-  ) {
+  @action private updateModifiers(event: K['gesture'] | K['pointer'] | K['keyboard'] | K['touch']) {
+    if (!event.isPrimary) {
+      return
+    }
     if ('clientX' in event) {
       this.previousScreenPoint = this.currentScreenPoint
       this.currentScreenPoint = Vec.sub([event.clientX, event.clientY], this.containerOffset)
@@ -48,18 +49,7 @@ export class TLInputs<K extends TLEventMap> {
     }
   }
 
-  @action onWheel = (pagePoint: number[], event: K['wheel']) => {
-    // if (this.state === 'pinching') return
-    this.updateModifiers(event)
-    this.previousPoint = this.currentPoint
-    this.currentPoint = pagePoint
-    // start panning = true here in panCamera (called in TLApp)
-    this.state = 'panning'
-    // otherwise, set panning = false?
-  }
-
   @action onPointerDown = (pagePoint: number[], event: K['pointer']) => {
-    // if (this.pointerIds.size > 0) return
     this.pointerIds.add(event.pointerId)
     this.updateModifiers(event)
     this.originScreenPoint = this.currentScreenPoint
@@ -69,7 +59,7 @@ export class TLInputs<K extends TLEventMap> {
 
   @action onPointerMove = (
     pagePoint: number[],
-    event: K['gesture'] | K['pointer'] | K['keyboard'] | K['wheel'] | K['touch']
+    event: K['gesture'] | K['pointer'] | K['keyboard'] | K['touch']
   ) => {
     if (this.state === 'pinching') return
     if (this.state === 'panning') {
@@ -110,7 +100,7 @@ export class TLInputs<K extends TLEventMap> {
 
   @action onPinchStart = (
     pagePoint: number[],
-    event: K['gesture'] | K['pointer'] | K['keyboard'] | K['wheel'] | K['touch']
+    event: K['gesture'] | K['pointer'] | K['keyboard'] | K['touch']
   ) => {
     this.updateModifiers(event)
     this.state = 'pinching'
@@ -118,7 +108,7 @@ export class TLInputs<K extends TLEventMap> {
 
   @action onPinch = (
     pagePoint: number[],
-    event: K['gesture'] | K['pointer'] | K['keyboard'] | K['wheel'] | K['touch']
+    event: K['gesture'] | K['pointer'] | K['keyboard'] | K['touch']
   ) => {
     if (this.state !== 'pinching') return
     this.updateModifiers(event)
@@ -126,7 +116,7 @@ export class TLInputs<K extends TLEventMap> {
 
   @action onPinchEnd = (
     pagePoint: number[],
-    event: K['gesture'] | K['pointer'] | K['keyboard'] | K['wheel'] | K['touch']
+    event: K['gesture'] | K['pointer'] | K['keyboard'] | K['touch']
   ) => {
     if (this.state !== 'pinching') return
     this.updateModifiers(event)

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

@@ -4,7 +4,6 @@ import { observable, makeObservable, action } from 'mobx'
 export interface TLSettingsProps {
   mode: 'light' | 'dark'
   showGrid: boolean
-  isToolLocked: boolean
 }
 
 export class TLSettings implements TLSettingsProps {
@@ -14,7 +13,6 @@ export class TLSettings implements TLSettingsProps {
 
   @observable mode: 'dark' | 'light' = 'light'
   @observable showGrid = true
-  @observable isToolLocked = false
 
   @action update(props: Partial<TLSettingsProps>): void {
     Object.assign(this, props)

+ 0 - 13
tldraw/packages/core/src/lib/TLState.ts

@@ -204,18 +204,6 @@ export abstract class TLRootState<S extends TLShape, K extends TLEventMap>
       this.onExit?.(info)
     },
 
-    /**
-     * Respond to wheel events forwarded to the state by its parent. Run the current active child
-     * state's handler, then the state's own handler.
-     *
-     * @param info The event info from TLInputs.
-     * @param event The DOM event.
-     */
-    onWheel: (info, event) => {
-      this.onWheel?.(info, event)
-      this.forwardEvent('onWheel', info, event)
-    },
-
     /**
      * Respond to pointer down events forwarded to the state by its parent. Run the current active
      * child state's handler, then the state's own handler.
@@ -382,7 +370,6 @@ export abstract class TLRootState<S extends TLShape, K extends TLEventMap>
   onEnter?: TLStateEvents<S, K>['onEnter']
   onExit?: TLStateEvents<S, K>['onExit']
   onTransition?: TLStateEvents<S, K>['onTransition']
-  onWheel?: TLEvents<S, K>['wheel']
   onPointerDown?: TLEvents<S, K>['pointer']
   onPointerUp?: TLEvents<S, K>['pointer']
   onPointerMove?: TLEvents<S, K>['pointer']

+ 14 - 12
tldraw/packages/core/src/lib/TLViewport.ts

@@ -46,15 +46,6 @@ export class TLViewport {
     return this
   }
 
-  private _currentView = {
-    minX: 0,
-    minY: 0,
-    maxX: 1,
-    maxY: 1,
-    width: 1,
-    height: 1,
-  }
-
   @computed get currentView(): TLBounds {
     const {
       bounds,
@@ -82,10 +73,21 @@ export class TLViewport {
     return Vec.mul(Vec.add(point, camera.point), camera.zoom)
   }
 
-  pinchCamera = (point: number[], delta: number[], zoom: number): this => {
+  onZoom = (point: number[], zoom: number): this => {
+    return this.pinchZoom(point, [0, 0], zoom)
+  }
+
+  /**
+   * Pinch to a new zoom level, possibly together with a pan.
+   *
+   * @param point The current point under the cursor.
+   * @param delta The movement delta.
+   * @param zoom The new zoom level
+   */
+  pinchZoom = (point: number[], delta: number[], zoom: number): this => {
     const { camera } = this
-    zoom = Math.max(TLViewport.minZoom, Math.min(TLViewport.maxZoom, zoom))
     const nextPoint = Vec.sub(camera.point, Vec.div(delta, camera.zoom))
+    zoom = Vec.clamp(zoom, TLViewport.minZoom, TLViewport.maxZoom)
     const p0 = Vec.sub(Vec.div(point, camera.zoom), nextPoint)
     const p1 = Vec.sub(Vec.div(point, zoom), nextPoint)
     return this.update({ point: Vec.toFixed(Vec.add(nextPoint, Vec.sub(p1, p0))), zoom })
@@ -94,7 +96,7 @@ export class TLViewport {
   setZoom = (zoom: number) => {
     const { bounds } = this
     const center = [bounds.width / 2, bounds.height / 2]
-    this.pinchCamera(center, [0, 0], zoom)
+    this.onZoom(center, zoom)
   }
 
   zoomIn = () => {

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

@@ -363,7 +363,7 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
    * Get a svg group element that can be used to render the shape with only the props data. In the
    * base, draw any shape as a box. Can be overridden by subclasses.
    */
-  getShapeSVGJsx(opts: any) {
+  getShapeSVGJsx(_?: any) {
     // Do not need to consider the original point here
     const bounds = this.getBounds()
     const { stroke, strokeWidth, strokeType, opacity, fill, noFill, borderRadius } = this

+ 0 - 7
tldraw/packages/core/src/lib/tools/TLBoxTool/states/CreatingState.tsx

@@ -86,16 +86,9 @@ export class CreatingState<
     if (this.creatingShape) {
       this.app.setSelectedShapes([this.creatingShape as unknown as S])
     }
-    if (!this.app.settings.isToolLocked) {
-      this.app.transition('select')
-    }
     this.app.persist()
   }
 
-  onWheel: TLStateEvents<S, K>['onWheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onKeyDown: TLStateEvents<S>['onKeyDown'] = (info, e) => {
     switch (e.key) {
       case 'Escape': {

+ 0 - 7
tldraw/packages/core/src/lib/tools/TLDotTool/states/CreatingState.tsx

@@ -48,16 +48,9 @@ export class CreatingState<
         this.app.setSelectedShapes([shape])
       })
     }
-    if (!this.app.settings.isToolLocked) {
-      this.app.transition('select')
-    }
     this.app.persist()
   }
 
-  onWheel: TLStateEvents<S, K>['onWheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onKeyDown: TLStateEvents<S>['onKeyDown'] = (info, e) => {
     switch (e.key) {
       case 'Escape': {

+ 7 - 3
tldraw/packages/core/src/lib/tools/TLDrawTool/TLDrawTool.tsx

@@ -1,8 +1,8 @@
-import { type TLEventMap, TLCursor } from '../../../types'
+import { type TLEventMap, TLCursor, TLEvents } from '../../../types'
 import type { TLDrawShape, TLShape, TLDrawShapeProps } from '../../shapes'
 import type { TLApp } from '../../TLApp'
 import { TLTool } from '../../TLTool'
-import { IdleState, CreatingState } from './states'
+import { IdleState, CreatingState, PinchingState } from './states'
 
 export abstract class TLDrawTool<
   T extends TLDrawShape = TLDrawShape,
@@ -12,7 +12,7 @@ export abstract class TLDrawTool<
 > extends TLTool<S, K, R> {
   static id = 'draw'
 
-  static states = [IdleState, CreatingState]
+  static states = [IdleState, CreatingState, PinchingState]
 
   static initial = 'idle'
 
@@ -30,4 +30,8 @@ export abstract class TLDrawTool<
     new (props: TLDrawShapeProps): T
     id: string
   }
+
+  onPinchStart: TLEvents<S>['pinch'] = (info, event) => {
+    this.transition('pinching', { info, event })
+  }
 }

+ 5 - 5
tldraw/packages/core/src/lib/tools/TLDrawTool/states/CreatingState.tsx

@@ -1,5 +1,5 @@
 import { Vec } from '@tldraw/vec'
-import type { TLEventMap, TLStateEvents } from '../../../../types'
+import type { TLEventMap, TLEvents, TLStateEvents } from '../../../../types'
 import { lerp, uniqueId, PointUtils } from '../../../../utils'
 import type { TLShape, TLDrawShape } from '../../../shapes'
 import type { TLApp } from '../../../TLApp'
@@ -36,6 +36,10 @@ export class CreatingState<
     }
   }
 
+  onPinchStart: TLEvents<S>['pinch'] = (info, event) => {
+    this.tool.transition('pinching', { info, event })
+  }
+
   onEnter = () => {
     const { Shape, previousShape } = this.tool
     const { originPoint } = this.app.inputs
@@ -93,10 +97,6 @@ export class CreatingState<
     this.app.persist()
   }
 
-  onWheel: TLStateEvents<S, K>['onWheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onKeyDown: TLStateEvents<S>['onKeyDown'] = (info, e) => {
     switch (e.key) {
       case 'Escape': {

+ 3 - 4
tldraw/packages/core/src/lib/tools/TLDrawTool/states/IdleState.tsx

@@ -1,4 +1,4 @@
-import type { TLEventMap, TLStateEvents } from '../../../../types'
+import type { TLEventMap, TLEvents, TLStateEvents } from '../../../../types'
 import type { TLShape, TLDrawShape } from '../../../shapes'
 import type { TLApp } from '../../../TLApp'
 import { TLToolState } from '../../../TLToolState'
@@ -18,9 +18,8 @@ export class IdleState<
     this.tool.transition('creating')
   }
 
-  onPinchStart: TLStateEvents<S, K>['onPinchStart'] = (...args) => {
-    this.app.transition('select', { returnTo: this.app.currentState.id })
-    this.app._events.onPinchStart?.(...args)
+  onPinchStart: TLEvents<S>['pinch'] = (info, event) => {
+    this.tool.transition('pinching', { info, event })
   }
 
   onKeyDown: TLStateEvents<S>['onKeyDown'] = (info, e) => {

+ 41 - 0
tldraw/packages/core/src/lib/tools/TLDrawTool/states/PinchingState.ts

@@ -0,0 +1,41 @@
+import type { TLEventMap, TLEventInfo, TLEvents } from '../../../../types'
+import type { TLDrawShape, TLShape } from '../../../shapes'
+import type { TLApp } from '../../../TLApp'
+import { TLToolState } from '../../../TLToolState'
+import type { TLDrawTool } from '../TLDrawTool'
+
+type GestureInfo<
+  S extends TLShape,
+  K extends TLEventMap,
+  E extends TLEventInfo<S> = TLEventInfo<S>
+> = {
+  info: E & { delta: number[]; point: number[]; offset: number[] }
+  event: K['pointer'] | K['touch'] | K['keyboard'] | K['gesture']
+}
+
+export class PinchingState<
+  S extends TLShape,
+  T extends S & TLDrawShape,
+  K extends TLEventMap,
+  R extends TLApp<S, K>,
+  P extends TLDrawTool<T, S, K, R>
+> extends TLToolState<S, K, R, P> {
+  static id = 'pinching'
+
+  private origin: number[] = [0, 0]
+
+  private prevDelta: number[] = [0, 0]
+
+  onEnter = (info: GestureInfo<S, K>) => {
+    this.prevDelta = info.info.delta
+    this.origin = info.info.point
+  }
+
+  onPinch: TLEvents<S>['pinch'] = info => {
+    this.app.viewport.pinchZoom(info.point, info.delta, info.delta[2])
+  }
+
+  onPinchEnd: TLEvents<S>['pinch'] = () => {
+    this.tool.transition('idle')
+  }
+}

+ 1 - 0
tldraw/packages/core/src/lib/tools/TLDrawTool/states/index.ts

@@ -1,2 +1,3 @@
 export * from './CreatingState'
 export * from './IdleState'
+export * from './PinchingState'

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLEraseTool/states/ErasingState.tsx

@@ -39,10 +39,6 @@ export class ErasingState<
     this.tool.transition('idle')
   }
 
-  onWheel: TLStateEvents<S, K>['onWheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onKeyDown: TLStateEvents<S>['onKeyDown'] = (info, e) => {
     switch (e.key) {
       case 'Escape': {

+ 5 - 1
tldraw/packages/core/src/lib/tools/TLMoveTool/TLMoveTool.ts

@@ -1,4 +1,4 @@
-import { type TLEventMap, TLCursor, type TLStateEvents } from '../../../types'
+import { type TLEventMap, TLCursor, type TLStateEvents, TLEvents } from '../../../types'
 import type { TLShape } from '../../shapes'
 import type { TLApp } from '../../TLApp'
 import { TLTool } from '../../TLTool'
@@ -32,4 +32,8 @@ export class TLMoveTool<
       }
     }
   }
+
+  onPinchStart: TLEvents<S>['pinch'] = (info, event) => {
+    this.transition('pinching', { info, event })
+  }
 }

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLMoveTool/states/IdleState.tsx

@@ -22,10 +22,6 @@ export class IdleState<
     }
   }
 
-  onPinchStart: TLEvents<S>['pinch'] = (info, event) => {
-    this.tool.transition('pinching', { info, event })
-  }
-
   onPointerDown: TLStateEvents<S, K>['onPointerDown'] = (info, e) => {
     if (info.order) return
     this.tool.transition('panning')

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLMoveTool/states/PanningState.tsx

@@ -1,5 +1,5 @@
 import Vec from '@tldraw/vec'
-import { type TLEventMap, TLCursor, type TLStateEvents } from '../../../../types'
+import { type TLEventMap, TLCursor, type TLStateEvents, TLEvents } from '../../../../types'
 import type { TLShape } from '../../../shapes'
 import type { TLApp } from '../../../TLApp'
 import { TLToolState } from '../../../TLToolState'

+ 2 - 2
tldraw/packages/core/src/lib/tools/TLMoveTool/states/PinchingState.ts

@@ -10,7 +10,7 @@ type GestureInfo<
   E extends TLEventInfo<S> = TLEventInfo<S>
 > = {
   info: E & { delta: number[]; point: number[]; offset: number[] }
-  event: K['wheel'] | K['pointer'] | K['touch'] | K['keyboard'] | K['gesture']
+  event: K['pointer'] | K['touch'] | K['keyboard'] | K['gesture']
 }
 
 export class PinchingState<
@@ -31,7 +31,7 @@ export class PinchingState<
   }
 
   onPinch: TLEvents<S>['pinch'] = info => {
-    this.app.viewport.pinchCamera(info.point, [0, 0], info.offset[0])
+    this.app.viewport.pinchZoom(info.point, info.delta, info.delta[2])
   }
 
   onPinchEnd: TLEvents<S>['pinch'] = () => {

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

@@ -32,10 +32,6 @@ export class BrushingState<
     this.tree.clear()
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const {
       inputs: { shiftKey, ctrlKey, originPoint, currentPoint },

+ 2 - 31
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PinchingState.ts

@@ -10,7 +10,7 @@ type GestureInfo<
   E extends TLEventInfo<S> = TLEventInfo<S>
 > = {
   info: E & { delta: number[]; point: number[]; offset: number[] }
-  event: K['wheel'] | K['pointer'] | K['touch'] | K['keyboard'] | K['gesture']
+  event: K['pointer'] | K['touch'] | K['keyboard'] | K['gesture']
 }
 
 export class PinchingState<
@@ -22,13 +22,7 @@ export class PinchingState<
   static id = 'pinching'
 
   onPinch: TLEvents<S>['pinch'] = (info, event: any) => {
-    const { camera } = this.app.viewport
-
-    // Normalize the value of deltaZ from raw WheelEvent
-    const deltaZ = normalizeWheel(event)[2] * 0.01
-    if (deltaZ === 0) return
-    const zoom = camera.zoom - deltaZ * camera.zoom
-    this.app.viewport.pinchCamera(info.point, [0, 0], zoom)
+    this.app.viewport.pinchZoom(info.point, info.delta, info.delta[2])
   }
 
   onPinchEnd: TLEvents<S>['pinch'] = () => {
@@ -39,26 +33,3 @@ export class PinchingState<
     this.tool.transition('idle')
   }
 }
-
-// Adapted from https://stackoverflow.com/a/13650579
-function normalizeWheel(event: WheelEvent) {
-  const MAX_ZOOM_STEP = 10
-  const { deltaY, deltaX } = event
-
-  let deltaZ = 0
-
-  if (event.ctrlKey || event.metaKey) {
-    const signY = Math.sign(event.deltaY)
-    const absDeltaY = Math.abs(event.deltaY)
-
-    let dy = deltaY
-
-    if (absDeltaY > MAX_ZOOM_STEP) {
-      dy = MAX_ZOOM_STEP * signY
-    }
-
-    deltaZ = dy
-  }
-
-  return [deltaX, deltaY, deltaZ]
-}

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingBoundsBackgroundState.ts

@@ -15,10 +15,6 @@ export class PointingBoundsBackgroundState<
 
   cursor = TLCursor.Move
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
     if (Vec.dist(currentPoint, originPoint) > 5) {

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

@@ -22,10 +22,6 @@ export class PointingCanvasState<
     }
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
     if (Vec.dist(currentPoint, originPoint) > 5) {

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingHandleState.ts

@@ -21,10 +21,6 @@ export class PointingHandleState<
     this.info = info
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
     if (Vec.dist(currentPoint, originPoint) > 5) {

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingMinimapState.ts

@@ -65,10 +65,6 @@ export class PointingMinimapState<
     }
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = (info, e) => {
     const newCameraPoint = this.getCameraPoint([e.clientX, e.clientY])
     if (newCameraPoint) {

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingResizeHandleState.ts

@@ -26,10 +26,6 @@ export class PointingResizeHandleState<
     this.app.cursors.reset()
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
     if (Vec.dist(currentPoint, originPoint) > 5) {

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingRotateHandleState.ts

@@ -31,10 +31,6 @@ export class PointingRotateHandleState<
     this.updateCursor()
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
     if (Vec.dist(currentPoint, originPoint) > 5) {

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingSelectedShapeState.ts

@@ -29,10 +29,6 @@ export class PointingSelectedShapeState<
     this.pointedSelectedShape = undefined
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
     if (Vec.dist(currentPoint, originPoint) > 5) {

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingShapeBehindBoundsState.ts

@@ -19,10 +19,6 @@ export class PointingShapeBehindBoundsState<
     this.info = info
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
     if (Vec.dist(currentPoint, originPoint) > 5) {

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingShapeState.ts

@@ -25,10 +25,6 @@ export class PointingShapeState<
     }
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
     if (Vec.dist(currentPoint, originPoint) > 5) {

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/ResizingState.ts

@@ -112,10 +112,6 @@ export class ResizingState<
     this.app.history.resume()
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const {
       inputs: { altKey, shiftKey, ctrlKey, originPoint, currentPoint },

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/RotatingState.ts

@@ -72,10 +72,6 @@ export class RotatingState<
     this.snapshot = {}
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const {
       selectedShapes,

+ 0 - 4
tldraw/packages/core/src/lib/tools/TLSelectTool/states/TranslatingState.ts

@@ -137,10 +137,6 @@ export class TranslatingState<
     this.initialClonePoints = {}
   }
 
-  onWheel: TLEvents<S>['wheel'] = (info, e) => {
-    this.onPointerMove(info, e)
-  }
-
   onPointerMove: TLEvents<S>['pointer'] = () => {
     this.moveSelectedShapesToPointer()
   }

+ 0 - 1
tldraw/packages/core/src/types/TLEventHandlers.ts

@@ -8,7 +8,6 @@ export interface TLEventHandlers<
   K extends TLEventMap = TLEventMap,
   E extends TLEventInfo<S> = TLEventInfo<S>
 > {
-  onWheel: TLEvents<S, K, E>['wheel']
   onPointerDown: TLEvents<S, K, E>['pointer']
   onPointerUp: TLEvents<S, K, E>['pointer']
   onPointerMove: TLEvents<S, K, E>['pointer']

+ 0 - 1
tldraw/packages/core/src/types/TLEventMap.ts

@@ -12,7 +12,6 @@
 import type { AnyObject } from './types'
 
 export interface TLEventMap {
-  wheel: AnyObject
   pointer: AnyObject
   touch: AnyObject
   keyboard: AnyObject

+ 2 - 3
tldraw/packages/core/src/types/TLEvents.ts

@@ -8,11 +8,10 @@ export interface TLEvents<
   K extends TLEventMap = TLEventMap,
   E extends TLEventInfo<S> = TLEventInfo<S>
 > {
-  wheel: (info: E & { delta: number[]; point: number[] }, event: K['wheel']) => void
   pinch: (
     info: E & { delta: number[]; point: number[]; offset: number[] },
-    event: K['wheel'] | K['pointer'] | K['touch'] | K['keyboard'] | K['gesture']
+    event: K['pointer'] | K['touch'] | K['keyboard'] | K['gesture']
   ) => void
-  pointer: (info: E, event: K['pointer'] | K['wheel']) => void
+  pointer: (info: E, event: K['pointer']) => void
   keyboard: (info: E, event: K['keyboard']) => void
 }

+ 8 - 0
tldraw/packages/core/src/utils/index.ts

@@ -81,6 +81,14 @@ export function isDarwin(): boolean {
   return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
 }
 
+/**
+ * Migrated from frontend.util/safari?
+ */
+export function isSafari(): boolean {
+  const ua = window.navigator.userAgent.toLowerCase()
+  return ua.includes('webkit') && !ua.includes('chrome')
+}
+
 /**
  * Get whether an event is command (mac) or control (pc).
  *

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

@@ -20,6 +20,12 @@ export function useCanvasEvents() {
     const onPointerDown: TLReactCustomEvents['pointer'] = e => {
       const { order = 0 } = e
       if (!order) e.currentTarget?.setPointerCapture(e.pointerId)
+
+      if (!e.isPrimary) {
+        // ignore secondary pointers (in multi-touch scenarios)
+        return
+      }
+
       callbacks.onPointerDown?.({ type: TLTargetType.Canvas, order }, e)
 
       const now = Date.now()

+ 3 - 5
tldraw/packages/react/src/hooks/useDebounced.ts

@@ -5,11 +5,9 @@ export function useDebouncedValue<T>(value: T, ms = 0) {
   useEffect(() => {
     let canceled = false
     const handler = setTimeout(() => {
-      requestIdleCallback(() => {
-        if (!canceled) {
-          setDebouncedValue(value)
-        }
-      })
+      if (!canceled) {
+        setDebouncedValue(value)
+      }
     }, ms)
     return () => {
       canceled = true

+ 82 - 30
tldraw/packages/react/src/hooks/useGestureEvents.ts

@@ -2,7 +2,7 @@ import Vec from '@tldraw/vec'
 import type { Handler, WebKitGestureEvent } from '@use-gesture/core/types'
 import { useGesture } from '@use-gesture/react'
 import * as React from 'react'
-import { TLTargetType, TLViewport } from '@tldraw/core'
+import { isDarwin, TLTargetType, TLViewport } from '@tldraw/core'
 import { useRendererContext } from './useRendererContext'
 
 type PinchHandler = Handler<
@@ -13,72 +13,101 @@ type PinchHandler = Handler<
 export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
   const { viewport, inputs, callbacks } = useRendererContext()
 
+  const rOriginPoint = React.useRef<number[] | undefined>(undefined)
+  const rDelta = React.useRef<number[]>([0, 0])
+
   const events = React.useMemo(() => {
     const onWheel: Handler<'wheel', WheelEvent> = gesture => {
-      const { event, delta } = gesture
+      const { event } = gesture
       event.preventDefault()
-      if (inputs.state === 'pinching') return
-      if (Vec.isEqual(delta, [0, 0])) return
-      callbacks.onWheel?.(
-        {
-          type: TLTargetType.Canvas,
-          order: 0,
-          delta: gesture.delta,
-          point: inputs.currentPoint,
-        },
-        event
-      )
+
+      const [x, y, z] = normalizeWheel(event)
+
+      if (inputs.state === 'pinching') {
+        return
+      }
+
+      if ((event.altKey || event.ctrlKey || event.metaKey) && event.buttons === 0) {
+        const bounds = viewport.bounds
+        const point = inputs.currentScreenPoint ?? [bounds.width / 2, bounds.height / 2]
+        const delta = z / 100
+        const zoom = viewport.camera.zoom
+        viewport.onZoom(point, zoom - delta * zoom)
+        return
+      } else {
+        const delta = Vec.mul(
+          event.shiftKey && !isDarwin()
+            ? // shift+scroll = pan horizontally
+              [y, 0]
+            : // scroll = pan vertically (or in any direction on a trackpad)
+              [x, y],
+          0.8
+        )
+
+        if (Vec.isEqual(delta, [0, 0])) {
+          return
+        }
+
+        viewport.panCamera(delta)
+      }
     }
 
-    const onPinchStart: PinchHandler = gesture => {
+    const onPinchStart: PinchHandler = ({ event, delta, offset, origin }) => {
       const elm = ref.current
-      const { event } = gesture
+      if (event instanceof WheelEvent) return
       if (!(event.target === elm || elm?.contains(event.target as Node))) return
-      if (!['idle', 'panning'].includes(inputs.state)) return
       callbacks.onPinchStart?.(
         {
           type: TLTargetType.Canvas,
           order: 0,
-          delta: gesture.delta,
-          offset: gesture.offset,
-          point: Vec.sub(gesture.origin, inputs.containerOffset),
+          delta: [...delta, offset[0]],
+          offset: offset,
+          point: Vec.sub(origin, inputs.containerOffset),
         },
         event
       )
+      rOriginPoint.current = origin
+      rDelta.current = [0, 0]
     }
 
-    const onPinch: PinchHandler = gesture => {
+    const onPinch: PinchHandler = ({ event, offset, origin }) => {
       const elm = ref.current
-      const { event } = gesture
+      if (event instanceof WheelEvent) return
       if (!(event.target === elm || elm?.contains(event.target as Node))) return
-      if (inputs.state !== 'pinching') return
+      if (!rOriginPoint.current) {
+        rOriginPoint.current = origin
+      }
+      const delta = Vec.sub(rOriginPoint.current, origin)
+      const trueDelta = Vec.sub(delta, rDelta.current)
       callbacks.onPinch?.(
         {
           type: TLTargetType.Canvas,
           order: 0,
-          delta: gesture.delta,
-          offset: gesture.offset,
-          point: Vec.sub(gesture.origin, inputs.containerOffset),
+          delta: [...trueDelta, offset[0]],
+          offset: offset,
+          point: Vec.sub(origin, inputs.containerOffset),
         },
         event
       )
+      rDelta.current = delta
     }
 
-    const onPinchEnd: PinchHandler = gesture => {
+    const onPinchEnd: PinchHandler = ({ event, delta, offset, origin }) => {
       const elm = ref.current
-      const { event } = gesture
+      if (event instanceof WheelEvent) return
       if (!(event.target === elm || elm?.contains(event.target as Node))) return
       if (inputs.state !== 'pinching') return
       callbacks.onPinchEnd?.(
         {
           type: TLTargetType.Canvas,
           order: 0,
-          delta: gesture.delta,
-          offset: gesture.offset,
-          point: Vec.sub(gesture.origin, inputs.containerOffset),
+          delta: [0, 0, offset[0]],
+          offset: offset,
+          point: Vec.sub(origin, inputs.containerOffset),
         },
         event
       )
+      rDelta.current = [0, 0]
     }
 
     return {
@@ -102,3 +131,26 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
     },
   })
 }
+
+// Adapted from https://stackoverflow.com/a/13650579
+function normalizeWheel(event: WheelEvent) {
+  const MAX_ZOOM_STEP = 10
+  const { deltaY, deltaX } = event
+
+  let deltaZ = 0
+
+  if (event.ctrlKey || event.metaKey) {
+    const signY = Math.sign(event.deltaY)
+    const absDeltaY = Math.abs(event.deltaY)
+
+    let dy = deltaY
+
+    if (absDeltaY > MAX_ZOOM_STEP) {
+      dy = MAX_ZOOM_STEP * signY
+    }
+
+    deltaZ = dy
+  }
+
+  return [deltaX, deltaY, deltaZ]
+}

+ 3 - 0
tldraw/packages/react/src/hooks/usePreventNavigation.ts

@@ -14,6 +14,9 @@ export function usePreventNavigation(rCanvas: React.RefObject<HTMLDivElement>):
     }
 
     const preventNavigation = (event: TouchEvent) => {
+      if (event.touches.length === 0) {
+        return
+      }
       // Center point of the touch area
       const touchXPosition = event.touches[0].pageX
       // Size of the touch area

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

@@ -54,7 +54,6 @@ export function useSetup<
     if (onPaste) unsubs.push(app.subscribe('paste', onPaste))
     if (onCanvasDBClick) unsubs.push(app.subscribe('canvas-dbclick', onCanvasDBClick))
     // Kind of unusual, is this the right pattern?
-
     return () => unsubs.forEach(unsub => unsub())
   }, [app, onPersist, onSave, onSaveAs, onError])
 }

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

@@ -119,6 +119,9 @@ const tlcss = css`
     cursor: var(--tl-cursor) !important;
     box-sizing: border-box;
     color: var(--tl-foreground);
+    -webkit-user-select: none;
+    -webkit-touch-callout: none;
+    -webkit-user-drag: none;
   }
 
   .tl-overlay {

+ 1 - 1
tldraw/packages/react/src/hooks/useZoom.ts

@@ -9,7 +9,7 @@ export function useZoom(ref: React.RefObject<HTMLDivElement>) {
   React.useLayoutEffect(() => {
     return autorun(() => {
       const zoom = viewport.camera.zoom
-      if (app.inputs.state !== 'pinching') {
+      if (app.inputs.state !== 'pinching' && zoom != null) {
         ref.current?.style.setProperty('--tl-zoom', zoom.toString())
       }
     })

+ 0 - 1
tldraw/packages/react/src/types/TLReactCustomEvents.ts

@@ -1,7 +1,6 @@
 import type { TLReactEventMap } from './TLReactEventMap'
 
 export interface TLReactCustomEvents {
-  wheel: (event: TLReactEventMap['wheel'] & { order?: number }) => void
   pinch: (event: TLReactEventMap['gesture'] & { order?: number }) => void
   pointer: (event: React.PointerEvent & { order?: number }) => void
   keyboard: (event: TLReactEventMap['keyboard'] & { order?: number }) => void

+ 0 - 1
tldraw/packages/react/src/types/TLReactEventHandlers.ts

@@ -6,7 +6,6 @@ export interface TLReactEventHandlers<
   S extends TLReactShape = TLReactShape,
   E extends TLEventInfo<S> = TLEventInfo<S>
 > {
-  onWheel: TLEvents<S, TLReactEventMap, E>['wheel']
   onPointerDown: TLEvents<S, TLReactEventMap, E>['pointer']
   onPointerUp: TLEvents<S, TLReactEventMap, E>['pointer']
   onPointerMove: TLEvents<S, TLReactEventMap, E>['pointer']

+ 45 - 0
yarn.lock

@@ -1253,6 +1253,18 @@ atob@^2.1.2:
   resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
   integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
 
+autoprefixer@^10.4.13:
+  version "10.4.13"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.13.tgz#b5136b59930209a321e9fa3dca2e7c4d223e83a8"
+  integrity sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==
+  dependencies:
+    browserslist "^4.21.4"
+    caniuse-lite "^1.0.30001426"
+    fraction.js "^4.2.0"
+    normalize-range "^0.1.2"
+    picocolors "^1.0.0"
+    postcss-value-parser "^4.2.0"
+
 autoprefixer@^9.8.6:
   version "9.8.8"
   resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a"
@@ -1485,6 +1497,16 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.6, browserslist@^4
     node-releases "^2.0.6"
     update-browserslist-db "^1.0.5"
 
+browserslist@^4.21.4:
+  version "4.21.4"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987"
+  integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==
+  dependencies:
+    caniuse-lite "^1.0.30001400"
+    electron-to-chromium "^1.4.251"
+    node-releases "^2.0.6"
+    update-browserslist-db "^1.0.9"
+
 buffer-crc32@~0.2.3:
   version "0.2.13"
   resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@@ -1599,6 +1621,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001370:
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001393.tgz#1aa161e24fe6af2e2ccda000fc2b94be0b0db356"
   integrity sha512-N/od11RX+Gsk+1qY/jbPa0R6zJupEa0lxeBG598EbrtblxVCTJsQwbRBm6+V+rxpc5lHKdsXb9RY83cZIPLseA==
 
+caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426:
+  version "1.0.30001431"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795"
+  integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==
+
 [email protected]:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/capacitor-voice-recorder/-/capacitor-voice-recorder-4.0.0.tgz#41939aa21e68eb58301e781c217ad17dd48d2a34"
@@ -2487,6 +2514,11 @@ electron-to-chromium@^1.4.202:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.247.tgz#cc93859bc5fc521f611656e65ce17eae26a0fd3d"
   integrity sha512-FLs6R4FQE+1JHM0hh3sfdxnYjKvJpHZyhQDjc2qFq/xFvmmRt/TATNToZhrcGUFzpF2XjeiuozrA8lI0PZmYYw==
 
+electron-to-chromium@^1.4.251:
+  version "1.4.284"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592"
+  integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==
+
 [email protected]:
   version "19.0.12"
   resolved "https://registry.yarnpkg.com/electron/-/electron-19.0.12.tgz#73d11cc2a3e4dbcd61fdc1c39561e7a7911046e9"
@@ -2985,6 +3017,11 @@ for-own@^1.0.0:
   dependencies:
     for-in "^1.0.1"
 
+fraction.js@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
+  integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
+
 fragment-cache@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@@ -7460,6 +7497,14 @@ update-browserslist-db@^1.0.5:
     escalade "^3.1.1"
     picocolors "^1.0.0"
 
+update-browserslist-db@^1.0.9:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
+  integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
+  dependencies:
+    escalade "^3.1.1"
+    picocolors "^1.0.0"
+
 uri-js@^4.2.2:
   version "4.4.1"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác