ソースを参照

Merge branch 'whiteboards' into whiteboards

Peng Xiao 3 年 前
コミット
989aec04f1
95 ファイル変更1314 行追加1039 行削除
  1. 23 3
      .github/workflows/build-desktop-release.yml
  2. 0 46
      android/README.md
  3. 2 2
      android/app/build.gradle
  4. 29 3
      android/app/src/main/java/com/logseq/app/FsWatcher.java
  5. 33 4
      bb.edn
  6. 1 1
      deps/db/src/logseq/db/default.cljs
  7. 8 3
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  8. 1 1
      deps/graph-parser/src/logseq/graph_parser/config.cljs
  9. 1 1
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  10. 96 0
      docs/develop-logseq-on-mobile.md
  11. 1 0
      docs/develop-logseq.md
  12. 0 18
      docs/mobile.md
  13. 3 2
      e2e-tests/basic.spec.ts
  14. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  15. 0 4
      ios/App/App/capacitor.config.json
  16. 0 33
      ios/README.md
  17. 1 1
      resources/css/common.css
  18. 0 279
      resources/css/tldraw.css
  19. 1 1
      resources/package.json
  20. 3 26
      scripts/src/logseq/tasks/dev.clj
  21. 21 0
      scripts/src/logseq/tasks/dev/desktop.clj
  22. 73 0
      scripts/src/logseq/tasks/dev/mobile.clj
  23. 7 1
      scripts/src/logseq/tasks/util.clj
  24. 3 1
      shadow-cljs.edn
  25. 2 2
      src/electron/electron/core.cljs
  26. 24 5
      src/electron/electron/url.cljs
  27. 3 1
      src/electron/electron/window.cljs
  28. 26 1
      src/main/electron/listener.cljs
  29. 46 32
      src/main/frontend/components/block.cljs
  30. 1 0
      src/main/frontend/components/block.css
  31. 2 0
      src/main/frontend/components/external.cljs
  32. 11 8
      src/main/frontend/components/onboarding.cljs
  33. 95 41
      src/main/frontend/components/onboarding/setups.cljs
  34. 1 0
      src/main/frontend/components/search.cljs
  35. 1 1
      src/main/frontend/components/settings.cljs
  36. 3 2
      src/main/frontend/components/sidebar.cljs
  37. 9 0
      src/main/frontend/config.cljs
  38. 14 6
      src/main/frontend/db.cljs
  39. 15 0
      src/main/frontend/db/model.cljs
  40. 41 3
      src/main/frontend/dicts.cljc
  41. 1 1
      src/main/frontend/extensions/code.css
  42. 4 19
      src/main/frontend/extensions/html_parser.cljs
  43. 0 6
      src/main/frontend/fs.cljs
  44. 55 23
      src/main/frontend/fs/capacitor_fs.cljs
  45. 0 3
      src/main/frontend/fs/nfs.cljs
  46. 0 1
      src/main/frontend/fs/protocol.cljs
  47. 22 10
      src/main/frontend/handler/block.cljs
  48. 33 15
      src/main/frontend/handler/editor.cljs
  49. 118 1
      src/main/frontend/handler/external.cljs
  50. 1 1
      src/main/frontend/handler/file_sync.cljs
  51. 2 5
      src/main/frontend/handler/graph.cljs
  52. 18 16
      src/main/frontend/handler/page.cljs
  53. 77 16
      src/main/frontend/modules/shortcut/dicts.cljc
  54. 1 2
      src/main/frontend/state.cljs
  55. 15 7
      src/main/frontend/util/marker.cljs
  56. 1 1
      src/main/frontend/util/text.cljs
  57. 1 1
      src/main/frontend/version.cljs
  58. 7 2
      templates/config.edn
  59. 2 3
      tldraw/apps/tldraw-logseq/package.json
  60. 3 0
      tldraw/apps/tldraw-logseq/src/app.tsx
  61. 8 13
      tldraw/apps/tldraw-logseq/src/components/inputs/ColorInput.tsx
  62. 0 1
      tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts
  63. 11 0
      tldraw/apps/tldraw-logseq/src/hooks/useQuickAdd.ts
  64. 15 13
      tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx
  65. 0 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx
  66. 1 0
      tldraw/apps/tldraw-logseq/src/lib/shapes/text/TextLabel.tsx
  67. 0 11
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool.tsx
  68. 17 0
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/LogseqPortalTool.tsx
  69. 1 0
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/index.ts
  70. 68 0
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/CreatingState.tsx
  71. 24 0
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/IdleState.tsx
  72. 2 0
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/index.ts
  73. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/index.ts
  74. 1 1
      tldraw/apps/tldraw-logseq/tsup.config.ts
  75. 0 1
      tldraw/package.json
  76. 9 31
      tldraw/packages/core/src/lib/TLApp/TLApp.ts
  77. 2 2
      tldraw/packages/core/src/lib/TLHistory.ts
  78. 2 6
      tldraw/packages/core/src/lib/TLPage/TLPage.ts
  79. 3 3
      tldraw/packages/core/src/lib/shapes/TLDotShape/TLDotShape.tsx
  80. 10 19
      tldraw/packages/core/src/lib/shapes/TLShape/TLShape.tsx
  81. 2 3
      tldraw/packages/core/src/lib/tools/TLDotTool/TLDotTool.ts
  82. 3 10
      tldraw/packages/core/src/lib/tools/TLDotTool/states/CreatingState.tsx
  83. 2 2
      tldraw/packages/core/src/lib/tools/TLDotTool/states/IdleState.tsx
  84. 0 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/BrushingState.ts
  85. 2 15
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts
  86. 0 16
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingBoundsBackgroundState.ts
  87. 1 17
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingCanvasState.ts
  88. 14 2
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingSelectedShapeState.ts
  89. 0 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/ResizingState.ts
  90. 4 0
      tldraw/packages/core/src/types/types.ts
  91. 1 1
      tldraw/packages/react/src/components/Canvas/Canvas.tsx
  92. 3 1
      tldraw/packages/react/src/hooks/useSetup.ts
  93. 0 1
      tldraw/packages/react/src/lib/TLReactShape.tsx
  94. 1 0
      tldraw/packages/react/src/types/TLReactSubscriptions.tsx
  95. 140 196
      tldraw/yarn.lock

+ 23 - 3
.github/workflows/build-desktop-release.yml

@@ -6,12 +6,16 @@ on:
   workflow_dispatch:
     inputs:
       build-target:
-        description: 'Build Target ("nightly"/"beta"/"non-release")'
-        type: string
+        description: 'Build Target (Release Type)'
+        type: choice
         required: true
+        options:
+          - beta
+          - nightly
+          - non-release
         default: "beta"
       git-ref:
-        description: "Release Git Ref(master)"
+        description: "Release Git Ref (Which branch or tag to build?)"
         required: true
         default: "master"
       is-draft:
@@ -24,6 +28,16 @@ on:
         type: boolean
         required: true
         default: true
+      enable-file-sync:
+        description: 'Build with file sync support'
+        type: boolean
+        required: true
+        default: false
+      enable-plugins:
+        description: 'Build with plugin system support'
+        type: boolean
+        required: true
+        default: true
   schedule: # Every workday at the 2 P.M. (UTC) we run a scheduled nightly build
     - cron: '0 14 * * MON-FRI'
 
@@ -87,6 +101,12 @@ jobs:
         run: |
           sed -i 's/defonce version ".*"/defonce version "${{ steps.ref.outputs.version }}"/g' src/main/frontend/version.cljs
 
+      - name: Set Build Environment Variables (only when workflow_dispath)
+        if: ${{ github.event_name == 'workflow_dispatch' }}
+        run: |
+          echo "ENABLE_FILE_SYNC=${{ github.event.inputs.enable-file-sync }}" >> $GITHUB_ENV
+          echo "ENABLE_PLUGINS=${{ github.event.inputs.enable-plugins }}" >> $GITHUB_ENV
+
       - name: Compile CLJS
         run: yarn install && gulp build && yarn cljs:release-electron
 

+ 0 - 46
android/README.md

@@ -1,46 +0,0 @@
-## Set up development environment 
-* Install Android studio [^1] and SDK (newer than 30) tools
-   Note: for M1 MacBook users.
-   - Download version **Mac with Apple Chip** 
-   - unzip it and move **Android Studio.app** file to **Applications**, or you will get the following error later.
-     ```
-     [error] Unable to launch Android Studio. Is it installed?
-        Attempted to open Android Studio at: /Applications/Android Studio.app
-        You can configure this with the CAPACITOR_ANDROID_STUDIO_PATH environment variable.
-     ```
-* In Android Studio, open **Tools** -> **SDK Manager** to install other SDK tools [^2].
-  > In the SDK Tools tab, make sure to install at least the following:
-  >> - Android SDK Build-Tools
-  >> - Android SDK Command-line Tools
-  >> - Android Emulator
-  >> - Android SDK Platform-Tools
-
-## Build the development app in Android emulator
-* Replace `server url` with your local-ip-address:3001 (run ifconfig to check) in *capacitor.config.ts*.
-* Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
-* Run `npx cap sync android` in another termimal (all-in-one cmd).
-* In Android Studio, open **Tools** -> **AVD Manager** to create Android Virtual Device (AVD), and lanuch it in the emulator.
-* In Android Studio, open **Run** -> **Run** to run Logseq.
-* After logseq startup in Android virtual device, repl should be able to connect
-* For browser console print and devtool remote debug, open chrome, type url chrome://inspect/#devices, you should see your device there, click inspect
-
-## Build a release and install it to your android device 
-* Comment in `server url` in *capacitor.config.ts*.
-* Connect your device to PC.
-* Run `yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync android && npx cap run android`
-
-## Build a apk
-* Comment out `server url` in *capacitor.config.ts*.
-* Run `yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync android`.
-* In Android Studio, open **Build** -> **Build Bundles / APKs** -> **Build APKs**.
-* Get your apk in `android/app/build/apk/debug`.
-
-[^1] https://developer.android.com/studio/index.html
-
-[^2] https://capacitorjs.com/docs/getting-started/environment-setup
-
-## Develop without opening Android Studio
-1. brew install gradle
-2. make sure java version using 11
-3. cd web/android && gradle wrapper
-4. install android sdk 30

+ 2 - 2
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 28
-        versionName "0.7.4"
+        versionCode 29
+        versionName "0.7.5"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 29 - 3
android/app/src/main/java/com/logseq/app/FsWatcher.java

@@ -7,6 +7,7 @@ import android.system.Os;
 import android.system.StructStat;
 import android.util.Log;
 import android.os.FileObserver;
+
 import android.net.Uri;
 
 import java.io.*;
@@ -133,7 +134,6 @@ public class FsWatcher extends Plugin {
         File f = new File(path);
         obj.put("path", Uri.fromFile(f));
         obj.put("dir", mPath);
-        Log.i("FsWatcher", "prepare event " + obj);
 
         switch (event) {
             case FileObserver.CLOSE_WRITE:
@@ -144,6 +144,7 @@ public class FsWatcher extends Plugin {
                 } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
+                Log.i("FsWatcher", "prepare event " + obj);
                 obj.put("content", content);
                 break;
             case FileObserver.MOVED_TO:
@@ -155,13 +156,20 @@ public class FsWatcher extends Plugin {
                 } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
+                Log.i("FsWatcher", "prepare event " + obj);
                 obj.put("content", content);
                 break;
             case FileObserver.MOVE_SELF:
             case FileObserver.MOVED_FROM:
             case FileObserver.DELETE:
             case FileObserver.DELETE_SELF:
-                obj.put("event", "unlink");
+                if (f.exists()) {
+                    Log.i("FsWatcher", "abandon notification due to file exists");
+                    return;
+                } else {
+                    obj.put("event", "unlink");
+                }
+                Log.i("FsWatcher", "prepare event " + obj);
                 break;
             default:
                 // unreachable?
@@ -218,7 +226,25 @@ public class FsWatcher extends Plugin {
                 Log.d("FsWatcher", "got path=" + path + " event=" + event);
                 if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) {
                     String fullPath = mPath + "/" + path;
-                    FsWatcher.this.onObserverEvent(event, fullPath);
+                    if (event == FileObserver.MOVE_SELF || event == FileObserver.MOVED_FROM ||
+                        event == FileObserver.DELETE || event == FileObserver.DELETE_SELF) {
+                        Log.d("FsWatcher", "defer delete notification for " + path);
+                        Thread timer = new Thread() {
+                            @Override
+                            public void run() {
+                                try {
+                                    // delay 500ms then send, enough for most syncing net disks
+                                    Thread.sleep(500);
+                                    FsWatcher.this.onObserverEvent(event, fullPath);
+                                } catch (InterruptedException e) {
+                                    e.printStackTrace();
+                                }
+                            }
+                        };
+                        timer.start();
+                    } else {
+                        FsWatcher.this.onObserverEvent(event, fullPath);
+                    }
                 }
             }
         }

+ 33 - 4
bb.edn

@@ -13,20 +13,49 @@
  {clj-kondo/clj-kondo {:version "2022.02.09"}
   org.babashka/fswatcher {:version "0.0.3"}}
  :tasks
- {dev:watch
-  logseq.tasks.dev/watch
+ {dev:desktop-watch
+  logseq.tasks.dev.desktop/watch
 
   dev:open-dev-electron-app
-  logseq.tasks.dev/open-dev-electron-app
+  logseq.tasks.dev.desktop/open-dev-electron-app
 
   -dev:electron-start
-  {:depends [dev:watch dev:open-dev-electron-app]}
+  {:depends [dev:desktop-watch dev:open-dev-electron-app]}
 
   dev:electron-start
   {:doc "Start electron dev by watching assets and opening dev app"
    ;; Parallel execution - https://book.babashka.org/#parallel
    :task (run '-dev:electron-start {:parallel true})}
 
+  dev:app-watch
+  logseq.tasks.dev.mobile/app-watch
+
+  dev:npx-cap-run-ios
+  logseq.tasks.dev.mobile/npx-cap-run-ios
+
+  -dev:ios-app
+  {:depends [dev:app-watch dev:npx-cap-run-ios]}
+
+  dev:ios-app
+  {:doc "iOS development environment"
+   :task (run '-dev:ios-app {:parallel true})}
+
+  release:ios-app
+  logseq.tasks.dev.mobile/run-ios-release
+
+  dev:npx-cap-run-android
+  logseq.tasks.dev.mobile/npx-cap-run-android
+
+  -dev:android-app
+  {:depends [dev:app-watch dev:npx-cap-run-android]}
+
+  dev:android-app
+  {:doc "Android development environment"
+   :task (run '-dev:android-app {:parallel true})}
+
+  release:android-app
+  logseq.tasks.dev.mobile/run-android-release
+
   dev:validate-local-storage
   logseq.tasks.spec/validate-local-storage
 

+ 1 - 1
deps/db/src/logseq/db/default.cljs

@@ -2,7 +2,7 @@
   (:require [clojure.string :as string]))
 
 (defonce built-in-pages-names
-  #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C" "Favorites" "Contents" "card"})
+  #{"NOW" "LATER" "DOING" "DONE" "CANCELED" "CANCELLED" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C" "Favorites" "Contents" "card"})
 
 (def built-in-pages
   (mapv (fn [p]

+ 8 - 3
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -237,6 +237,10 @@
 (defn page-name->map
   "Create a page's map structure given a original page name (string).
    map as input is supported for legacy compatibility.
+   with-id?: if true, assign uuid to the map structure.
+    if the page entity already exists, no-op.
+    else, if with-id? is a uuid, the uuid is used.
+    otherwise, generate a uuid.
    with-timestamp?: assign timestampes to the map structure.
     Useful when creating new pages from references or namespaces,
     as there's no chance to introduce timestamps via editing in page"
@@ -253,9 +257,10 @@
        {:block/name page-name
         :block/original-name original-page-name}
        (when with-id?
-         (if page-entity
-           {:block/uuid (:block/uuid page-entity)}
-           {:block/uuid (d/squuid)}))
+         (let [new-uuid (cond page-entity      (:block/uuid page-entity)
+                              (uuid? with-id?) with-id?
+                              :else            (d/squuid))]
+           {:block/uuid new-uuid}))
        (when namespace?
          (let [namespace (first (gp-util/split-last "/" original-page-name))]
            (when-not (string/blank? namespace)

+ 1 - 1
deps/graph-parser/src/logseq/graph_parser/config.cljs

@@ -31,7 +31,7 @@
 (defn text-formats
   []
   #{:json :org :md :yml :dat :asciidoc :rst :txt :markdown :adoc :html :js :ts :edn :clj :ml :rb :ex :erl :java :php :c :css
-    :excalidraw})
+    :excalidraw :tldr :sh})
 
 (defn img-formats
   []

+ 1 - 1
deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs

@@ -142,7 +142,7 @@
   ;; only increase over time as the docs graph rarely has deletions
   (testing "Counts"
     (is (= 211 (count files)) "Correct file count")
-    (is (= 40943 (count (d/datoms db :eavt))) "Correct datoms count")
+    (is (= 40945 (count (d/datoms db :eavt))) "Correct datoms count")
 
     (is (= 3600
            (ffirst

+ 96 - 0
docs/develop-logseq-on-mobile.md

@@ -0,0 +1,96 @@
+# iOS development
+
+## Installation
+- Install Xcode 13 from App Store.
+- Install [CocoaPods](https://cocoapods.org/)
+  ```shell
+  sudo gem install cocoapods
+  ```
+  Note: use the following commands from *ios/App* directory to fix **ffi_c.bundle** related issue for M1 MacBook [^1].
+  ```shell
+  sudo arch -x86_64 gem install ffi
+  arch -x86_64 pod install
+  ```
+ 
+## Set up development environment
+### Build the development app
+- comment in `server` section in **capacitor.config.ts**, and replace `process.env.LOGSEQ_APP_ASERVER_URL` with your `http://your-local-ip-address:3001` (run `ifconfig` to check).
+    ```typescript
+    server: {
+        url: "process.env.LOGSEQ_APP_ASERVER_URL",
+        cleartext: true
+        } 
+    ```
+- Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
+- Run `npx cap sync ios` in another termimal to copy web assets from public to *ios/App/App/public*, and create *capacitor.config.json* in *ios/App/App*, and update iOS plugins.
+- Connect your iOS device to MacBook.
+- Run `npx cap open ios` to open Logseq project in Xcode, and build the app there.
+
+or, you can run `bb dev:ios-app` to do those steps with one command if you are on MacOS. To download bb, see https://github.com/babashka/babashka#installation. Also, in order to use mobile bb tasks on macOS, `gsed` needs to be installed in your system (run `brew install gnu-sed` to install).
+
+### Build the release app
+- Comment out `server` section above in **capacitor.config.ts**.
+- Connect your iOS device to MacBook.
+- Run `yarn run-ios-release` to install the release app to your iOS device.
+
+or, you can run `bb release:ios-app` to do those steps with one command.
+
+[^1] https://github.com/CocoaPods/CocoaPods/issues/10220#issuecomment-730963835
+
+
+# Android development  
+## Installation
+- Install Android studio [^1] and SDK (newer than 30) tools
+  Note: for M1 MacBook users.
+  - Download version **Mac with Apple Chip** 
+  - unzip it and move **Android Studio.app** file to **Applications**, or you will get the following error later.
+    ```
+     [error] Unable to launch Android Studio. Is it installed?
+        Attempted to open Android Studio at: /Applications/Android Studio.app
+        You can configure this with the CAPACITOR_ANDROID_STUDIO_PATH environment variable.
+     ```
+- In Android Studio, open **Tools** -> **SDK Manager** to install other SDK tools [^2].
+  > In the SDK Tools tab, make sure to install at least the following:
+  >> - Android SDK Build-Tools
+  >> - Android SDK Command-line Tools
+  >> - Android Emulator
+  >> - Android SDK Platform-Tools
+
+## Set up development environment
+### Build the development app
+- comment in `server` section in **capacitor.config.ts**, and replace `process.env.LOGSEQ_APP_ASERVER_URL` with your `http://your-local-ip-address:3001` (run `ifconfig` to check).
+    ```typescript
+    server: {
+        url: "process.env.LOGSEQ_APP_ASERVER_URL",
+        cleartext: true
+        } 
+    ```
+- Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
+- Run `npx cap sync android` in another termimal.
+- Run `npx cap run android` to install app into your device.
+
+or, you can run `bb dev:android-app` to do those steps with one command if you are on macOS.
+
+Then,
+- In Android Studio, open **Tools** -> **AVD Manager** to create Android Virtual Device (AVD), and lanuch it in the emulator.
+- In Android Studio, open **Run** -> **Run** to run Logseq.
+- After logseq startup in Android virtual device, repl should be able to connect
+- For browser console print and devtool remote debug, open chrome, type url chrome://inspect/#devices, you should see your device there, click inspect
+
+
+### Build a release and install it to your android device 
+- Comment out `server` section above in **capacitor.config.ts**.
+- Connect your device to PC.
+- Run `yarn run-android-release`.
+
+or, you can run `bb release:android-app` to do those steps with one command.
+
+### Build an apk
+- Comment out `server` section above in **capacitor.config.ts**.
+- Run `yarn run-android-release`
+
+or, you can run `bb release:android-app` to do those steps with one command.
+
+Then,
+- In Android Studio, open **Build** -> **Build Bundles / APKs** -> **Build APKs**.
+- Get your apk in `android/app/build/apk/debug`.

+ 1 - 0
docs/develop-logseq.md

@@ -44,6 +44,7 @@ yarn install && cd static && yarn install && cd ..
 ```bash
 yarn watch
 # Wait until watch is finished building and then in a different shell
+# If you have opened desktop logseq, you should close it. Otherwise, the following command will fail.
 yarn dev-electron-app
 ```
 

+ 0 - 18
docs/mobile.md

@@ -1,18 +0,0 @@
-## install list:
-* Android studio
-* SDK 30
-* other sdk tools in Android studio preference setting https://capacitorjs.com/docs/getting-started/environment-setup
-* change the server url in `capacitor.config.ts` with your local ip:3001 (run ifconfig to check)
-* run `yarn && yarn app-watch`
-* in another console, run `npx cap open android`
-* create Android virtual device in Android studio
-* click the run button in Android stutio to run the project
-* after logseq startup in Android virtual device, repl should be able to connect
-* for browser console print and devtool remote debug, open chrome, type url chrome://inspect/#devices, you should see your device there, click inspect
-
-
-## Develop without opening Android Studio
-1. brew install gradle
-2. make sure java version using 11
-3. cd web/android && gradle wrapper
-4. install android sdk 30

+ 3 - 2
e2e-tests/basic.spec.ts

@@ -120,7 +120,8 @@ test('template', async ({ page, block }) => {
 
   await createRandomPage(page)
 
-  await block.mustFill('template test\ntemplate:: ' + randomTemplate)
+  await block.mustFill('template test\ntemplate:: ')
+  await page.keyboard.type(randomTemplate, {delay: 100})
   await page.keyboard.press('Enter')
   await block.clickNext()
 
@@ -195,7 +196,7 @@ test('auto completion and auto pair', async ({ page, block }) => {
 
   // {{
   await block.mustType('type {{', { toBe: 'type {{}}' })
-
+  await page.waitForTimeout(100);
   // ((
   await block.clickNext()
 

+ 4 - 4
ios/App/App.xcodeproj/project.pbxproj

@@ -542,7 +542,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.7.4;
+				MARKETING_VERSION = 0.7.5;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -568,7 +568,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.7.4;
+				MARKETING_VERSION = 0.7.5;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -593,7 +593,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.7.4;
+				MARKETING_VERSION = 0.7.5;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -620,7 +620,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.7.4;
+				MARKETING_VERSION = 0.7.5;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 0 - 4
ios/App/App/capacitor.config.json

@@ -17,9 +17,5 @@
 	},
 	"ios": {
 		"scheme": "Logseq"
-	},
-	"server": {
-		"url": "http://192.168.1.59:3001",
-		"cleartext": true
 	}
 }

+ 0 - 33
ios/README.md

@@ -1,33 +0,0 @@
-Installation:
-
-* Install Xcode 13 from App Store.
-* Install [CocoaPods](https://cocoapods.org/)
-  ```shell
-  sudo gem install cocoapods
-  ```
-  Note: use the following commands from *ios/App* directory to fix **ffi_c.bundle** related issue for M1 MacBook [^1].
-  ```shell
-  sudo arch -x86_64 gem install ffi
-  arch -x86_64 pod install
-  ```
-* Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
-* Open Logseq project in Xcode by running the following command in termimal.
-  ```shell
-  npx cap open ios
-  ```
-  Note: for the first time after a fresh clone.
-  - Run `npx cap copy ios` to copy web assets from public to *ios/App/App/public*, and create *capacitor.config.json* in *ios/App/App*.
-  - Run `npx cap update ios` to update iOS plugins.
-  - Add the following code to *ios/App/App/capacitor.config.json*, and replace `server url` with your local-ip-address:3001 (run ifconfig to check)
-    ```json
-    "server": {
-        "url": "http://your-own-id-address:3001",
-        "cleartext": true} 
-    ```
-* Run logseq 
-  ```shell
-  npx cap run ios
-  ```
-  
-[^1] https://github.com/CocoaPods/CocoaPods/issues/10220#issuecomment-730963835
-  

+ 1 - 1
resources/css/common.css

@@ -878,7 +878,7 @@ h1.title {
 
 .block-highlight,
 .content .selected {
-  transition: background-color 0.15s;
+  transition: background-color 0.2s cubic-bezier(0, 1, 0, 1);
   background-color: var(--ls-block-highlight-color);
   padding: -1px;
 }

+ 0 - 279
resources/css/tldraw.css

@@ -1,279 +0,0 @@
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap');
-
-:root {
-  --color-panel: #ffffff;
-  --color-text: #000000;
-  --color-hover: #00000011;
-  --color-selectedStroke: rgb(42, 123, 253);
-  --color-selectedFill: rgba(66, 133, 244);
-  --color-selectedContrast: #ffffff;
-  --shadow-medium: 0px 0px 16px -1px rgba(0, 0, 0, 0.05), 0px 0px 16px -8px rgba(0, 0, 0, 0.09),
-    0px 0px 16px -12px rgba(0, 0, 0, 0.2);
-}
-
-.logseq-tldraw-wrapper {
-  width: 100%;
-  height: 100%;
-  position: relative;
-}
-
-.logseq-tldraw label {
-  font-family: 'Inter', Arial, Helvetica, sans-serif;
-}
-
-.logseq-tldraw button {
-  font-size: 13px;
-  font-family: 'Inter', Arial, Helvetica, sans-serif;
-  background: none;
-  border: none;
-  cursor: pointer;
-  border-radius: 2px;
-  padding: 4px 8px;
-}
-
-.logseq-tldraw .toolbar {
-  position: absolute;
-  top: 0;
-  width: 100%;
-  grid-row: 1;
-  display: flex;
-  align-items: center;
-  padding: 8px;
-  color: black;
-  z-index: 100000;
-  user-select: none;
-  background: white;
-  border-bottom: 1px solid black;
-  font-size: inherit;
-}
-
-.logseq-tldraw .contextbar {
-  pointer-events: all;
-  position: relative;
-  background-color: var(--color-panel);
-  padding: 8px 12px;
-  border-radius: 8px;
-  white-space: nowrap;
-  display: flex;
-  gap: 4px;
-  align-items: center;
-  font-size: 14px;
-  will-change: transform, contents;
-  box-shadow: var(--shadow-medium);
-  z-index: 1000;
-}
-
-.logseq-tldraw .statusbar {
-  position: absolute;
-  bottom: 0;
-  grid-row: 3;
-  width: 100%;
-  display: flex;
-  align-items: center;
-  padding: 8px;
-  color: black;
-  z-index: 100000;
-  user-select: none;
-  background: white;
-  border-top: 1px solid black;
-}
-
-.logseq-tldraw .input {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-}
-
-.logseq-tldraw .number-input {
-  width: 44px;
-  height: 24px;
-  padding: 2px;
-}
-
-.logseq-tldraw .color-input {
-  height: 24px;
-  padding: 0 2px;
-  background: none;
-  border-radius: 2px;
-}
-
-.logseq-tldraw .text-input {
-  height: 24px;
-  padding: 4px;
-  background: none;
-  border: 1px solid black;
-  border-radius: 2px;
-}
-
-.logseq-tldraw .input > label {
-  font-size: 10px;
-}
-
-.logseq-tldraw .primary-tools {
-  display: flex;
-  position: absolute;
-  bottom: 48px;
-  width: 100%;
-  height: 64px;
-  align-items: center;
-  justify-content: center;
-  pointer-events: none;
-  gap: 8px;
-  z-index: 10000;
-}
-
-.logseq-tldraw .panel {
-  background-color: var(--color-panel);
-  box-shadow: var(--shadow-medium);
-  pointer-events: all;
-}
-
-.floating-button {
-  background-color: var(--color-panel);
-  height: 32px;
-  width: 32px;
-  border-radius: 50%;
-  box-shadow: var(--shadow-medium);
-  overflow: hidden;
-}
-
-.logseq-tldraw .primary-tools .floating-panel {
-  display: flex;
-  border-radius: 128px;
-  overflow: hidden;
-  padding: 4px;
-}
-
-.logseq-tldraw .floating-panel > :nth-child(1) {
-  border-top-left-radius: 20px;
-  border-bottom-left-radius: 20px;
-}
-
-.logseq-tldraw .floating-panel > :nth-last-child(1) {
-  border-top-right-radius: 20px;
-  border-bottom-right-radius: 20px;
-}
-
-.logseq-tldraw .primary-tools .button {
-  position: relative;
-  height: 40px;
-  width: 40px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 13px;
-  font-family: 'Inter', Arial, Helvetica, sans-serif;
-  background: none;
-  border: none;
-  cursor: pointer;
-}
-
-.logseq-tldraw .primary-tools .button:hover {
-  background-color: var(--color-hover);
-}
-
-.logseq-tldraw .primary-tools .button[data-selected='true'] {
-  background-color: var(--color-selectedFill);
-  color: var(--color-selectedContrast);
-}
-
-.logseq-tldraw .floating-panel[data-tool-locked='true'] > .button[data-selected='true']::after {
-  content: '';
-  display: block;
-  height: 6px;
-  width: 6px;
-  border: 2px solid var(--color-selectedContrast);
-  background-color: var(--color-selectedFill);
-  position: absolute;
-  bottom: -4px;
-  left: calc(50% - 5px);
-  border-radius: 100%;
-}
-
-.logseq-tldraw .text-shape-wrapper {
-  position: absolute;
-  white-space: pre-wrap;
-  overflow-wrap: break-word;
-  width: auto;
-  border: 1px solid transparent;
-  margin: 0px;
-  padding: 0px;
-  z-index: 9999;
-  user-select: none;
-  width: 100%;
-  height: 100%;
-  z-index: 1;
-  min-height: 1;
-  min-width: 1;
-  line-height: 1;
-  outline: 0;
-  backface-visibility: hidden;
-  user-select: none;
-  pointer-events: all;
-  vertical-align: baseline;
-  -webkit-user-drag: none;
-  -webkit-user-select: none;
-  -webkit-touch-callout: none;
-}
-
-.logseq-tldraw .text-shape-content {
-  z-index: 1;
-  width: fit-content;
-  height: fit-content;
-  border: none;
-  resize: none;
-  margin: 0;
-  padding: inherit;
-  font-family: inherit;
-  font-size: inherit;
-  font-variant: inherit;
-  text-align: inherit;
-  min-height: inherit;
-  min-width: inherit;
-  line-height: inherit;
-  letter-spacing: inherit;
-  outline: 0;
-  white-space: inherit;
-  overflow-wrap: inherit;
-  font-weight: inherit;
-  overflow: hidden;
-  backface-visibility: hidden;
-  display: inline-block;
-  user-select: none;
-  -webkit-user-select: none;
-}
-
-.logseq-tldraw .text-shape-content[data-isediting='true'] {
-  visibility: hidden;
-  pointer-events: none;
-}
-
-.logseq-tldraw .text-shape-input {
-  position: absolute;
-  top: 0;
-  left: 0;
-  z-index: 1;
-  width: 100%;
-  height: 100%;
-  border: none;
-  resize: none;
-  padding: inherit;
-  font-family: inherit;
-  font-size: inherit;
-  font-variant: inherit;
-  text-align: inherit;
-  min-height: inherit;
-  min-width: inherit;
-  line-height: inherit;
-  letter-spacing: inherit;
-  outline: 0;
-  white-space: inherit;
-  overflow-wrap: inherit;
-  font-weight: inherit;
-  overflow: hidden;
-  backface-visibility: hidden;
-  display: inline-block;
-  pointer-events: all;
-  user-select: text;
-  -webkit-user-select: text;
-}

+ 1 - 1
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.7.4",
+  "version": "0.7.5",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",

+ 3 - 26
scripts/src/logseq/tasks/dev.clj

@@ -1,30 +1,7 @@
 (ns logseq.tasks.dev
-  "Tasks for development"
-  (:require [babashka.fs :as fs]
-            [babashka.tasks :refer [shell]]))
-
-(defn watch
-  "Watches environment to reload cljs, css and other assets"
-  []
-  (shell "yarn electron-watch"))
-
-(defn- file-modified-later-than?
-  [file comparison-instant]
-  (pos? (.compareTo (fs/file-time->instant (fs/last-modified-time file))
-                    comparison-instant)))
-
-;; Works whether yarn clean has been run before or not
-(defn open-dev-electron-app
-  "Opens dev-electron-app when watch process has built main.js"
-  []
-  (let [start-time (java.time.Instant/now)]
-    (dotimes [_n 1000]
-             (if (and (fs/exists? "static/js/main.js")
-                      (file-modified-later-than? "static/js/main.js" start-time))
-               (shell "yarn dev-electron-app")
-               (println "Waiting for app to build..."))
-             (Thread/sleep 1000))))
-
+  "Tasks for general development. For desktop or mobile development see their
+  namespaces"
+  (:require [babashka.tasks :refer [shell]]))
 
 (defn lint
   "Run all lint tasks

+ 21 - 0
scripts/src/logseq/tasks/dev/desktop.clj

@@ -0,0 +1,21 @@
+(ns logseq.tasks.dev.desktop
+  "Tasks for desktop (electron) development"
+  (:require [babashka.tasks :refer [shell]]
+            [babashka.fs :as fs]
+            [logseq.tasks.util :as task-util]))
+
+(defn watch
+  "Watches environment to reload cljs, css and other assets"
+  []
+  (shell "yarn electron-watch"))
+
+(defn open-dev-electron-app
+  "Opens dev-electron-app when watch process has built main.js"
+  []
+  (let [start-time (java.time.Instant/now)]
+    (dotimes [_n 1000]
+             (if (and (fs/exists? "static/js/main.js")
+                      (task-util/file-modified-later-than? "static/js/main.js" start-time))
+               (shell "yarn dev-electron-app")
+               (println "Waiting for app to build..."))
+             (Thread/sleep 1000))))

+ 73 - 0
scripts/src/logseq/tasks/dev/mobile.clj

@@ -0,0 +1,73 @@
+(ns logseq.tasks.dev.mobile
+  "Tasks for mobile development"
+  (:require [babashka.tasks :refer [shell]]
+            [babashka.fs :as fs]
+            [clojure.string :as string]
+            [logseq.tasks.util :as task-util]))
+
+(defn- open-dev-app
+  "Opens mobile app when watch process has built main.js"
+  [cmd]
+  (let [start-time (java.time.Instant/now)]
+    (loop [n 1000]
+      (if (and (fs/exists? "static/js/main.js")
+               (task-util/file-modified-later-than? "static/js/main.js" start-time))
+        (shell cmd)
+        (println "Waiting for app to build..."))
+      (Thread/sleep 1000)
+      (when-not (or (and (fs/exists? "ios/App/App/public/static/js/main.js")
+                      (task-util/file-modified-later-than? "ios/App/App/public/static/js/main.js" start-time))
+                    (and (fs/exists? "android/App/src/main/assets/public/static/js/main.js")
+                      (task-util/file-modified-later-than? "android/App/src/main/assets/public/static/js/main.js" start-time)))
+        (recur (dec n))))))
+
+(defn- set-system-env
+  "Updates capacitor.config.ts serve url with IP from ifconfig"
+  []
+  (let [ip (string/trim (:out (shell {:out :string} "ipconfig getifaddr en0")))
+        logseq-app-server-url (format "%s://%s:%s" "http" ip "3001")]
+    (println "Server URL:" logseq-app-server-url)
+    (shell "git checkout capacitor.config.ts")
+    (let [new-body (-> (slurp "capacitor.config.ts")
+                       (string/replace "// , server:" " , server:")
+                       (string/replace "//    url:" "    url:")
+                       (string/replace "process.env.LOGSEQ_APP_SERVER_URL"
+                                       (pr-str logseq-app-server-url))
+                       (string/replace "//    cleartext:" "    cleartext:")
+                       (string/replace "// }" " }"))]
+      (spit "capacitor.config.ts" new-body))))
+
+
+(defn app-watch
+  "Watches environment to reload cljs, css and other assets for mobile"
+  []
+  (println "set-system-env")
+  (set-system-env)
+  (doseq [cmd ["yarn clean"
+               "yarn app-watch"]]
+    (println cmd)
+    (shell cmd)))
+
+(defn npx-cap-run-ios
+  "Copy assets files to iOS build directory, and run app in Xcode"
+  []
+  (open-dev-app "npx cap sync ios")
+  (shell "npx cap open ios"))
+
+(defn npx-cap-run-android
+  "Copy assets files to Android build directory, and run app in Android Studio"
+  []
+  (open-dev-app "npx cap sync android")
+  (shell "npx cap open android"))
+
+(defn run-ios-release
+  "Build iOS app release"
+  []
+  (shell "git checkout capacitor.config.ts")
+  (shell "yarn run-ios-release"))
+
+(defn run-android-release
+  "Build Android app release"
+  []
+  (shell "git checkout capacitor.config.ts")
+  (shell "yarn run-android-release"))

+ 7 - 1
scripts/src/logseq/tasks/util.clj

@@ -1,6 +1,12 @@
 (ns logseq.tasks.util
   "Utils for tasks"
-  (:require [clojure.pprint :as pprint]))
+  (:require [clojure.pprint :as pprint]
+            [babashka.fs :as fs]))
+
+(defn file-modified-later-than?
+  [file comparison-instant]
+  (pos? (.compareTo (fs/file-time->instant (fs/last-modified-time file))
+                    comparison-instant)))
 
 (defn print-usage [arg-str]
   (println (format

+ 3 - 1
shadow-cljs.edn

@@ -37,7 +37,9 @@
                                                 "externs.js"]
                            :warnings           {:fn-deprecated false
                                                 :redef false}}
-        :closure-defines  {goog.debug.LOGGING_ENABLED      true}
+        :closure-defines  {goog.debug.LOGGING_ENABLED       true
+                           frontend.config/ENABLE-FILE-SYNC #shadow/env ["ENABLE_FILE_SYNC" :as :bool :default false]
+                           frontend.config/ENABLE-PLUGINS   #shadow/env ["ENABLE_PLUGINS"   :as :bool :default true]}
 
         ;; NOTE: electron, browser/mobile-app use different asset-paths.
         ;;   For browser/mobile-app devs, assets are located in /static/js(via HTTP root).

+ 2 - 2
src/electron/electron/core.cljs

@@ -114,7 +114,7 @@
                            ["css" "fonts" "icons" "img" "js"])))
                 export-css (. fs readFile export-or-custom-css-path)
                 _ (. fs writeFile (path/join static-dir "css" "export.css") export-css)
-                js-files ["main.js" "code-editor.js" "excalidraw.js"]
+                js-files ["main.js" "code-editor.js" "excalidraw.js" "tldraw.js"]
                 _ (p/all (map (fn [file]
                                 (. fs removeSync (path/join static-dir "js" file)))
                               js-files))
@@ -128,7 +128,7 @@
                 ;; TODO: ugly, replace with ls-files and filter with ".map"
                 _ (p/all (map (fn [file]
                                 (. fs removeSync (path/join static-dir "js" (str file ".map"))))
-                              ["main.js" "code-editor.js" "excalidraw.js" "age-encryption.js"]))]
+                              ["main.js" "code-editor.js" "excalidraw.js" "tldraw.js" "age-encryption.js"]))]
           (. dialog showMessageBox (clj->js {:message (str "Export public pages and publish assets to " root-dir " successfully")})))))))
 
 (defn setup-app-manager!

+ 24 - 5
src/electron/electron/url.cljs

@@ -29,7 +29,7 @@
                                       :payload (str "Failed to open link. Missing graph identifier after `logseq://graph/`.")})))
 
 (defn local-url-handler
-  "Given a URL with `graph identifier` as path, `page` (optional) and `block-id` 
+  "Given a URL with `graph identifier` as path, `page` (optional) and `block-id`
    (optional) as parameters, open the local graphs accordingly.
    `graph identifier` is the name of the graph to open, e.g. `lambda`"
   [^js win parsed-url force-new-window?]
@@ -45,10 +45,10 @@
           ;; TODO: allow open new window on specific page, without waiting for `graph ready` ipc then redirect to that page
         (when (or page-name block-id file)
           (let [redirect-f (fn [win' graph-name']
-                         (when (= graph-name graph-name')
-                           (send-to-renderer win' "redirectWhenExists" {:page-name page-name
-                                                                        :block-id block-id
-                                                                        :file file})))]
+                             (when (= graph-name graph-name')
+                               (send-to-renderer win' "redirectWhenExists" {:page-name page-name
+                                                                            :block-id block-id
+                                                                            :file file})))]
             (if open-new-window?
               (state/set-state! :window/once-graph-ready redirect-f)
               (do (win/switch-to-window! window-on-graph)
@@ -57,6 +57,22 @@
           (send-to-renderer win "openNewWindowOfGraph" graph-name)))
       (graph-identifier-error-handler graph-identifier))))
 
+(defn- x-callback-url-handler
+  [^js parsed-url]
+  (let [action (.-pathname parsed-url)]
+    (cond
+      (= action "/quickCapture")
+      (let [[url title content] (get-URL-decoded-params parsed-url ["url" "title" "content"])]
+        (send-to-renderer "quickCapture" {:url url
+                                          :title title
+                                          :content content}))
+
+      :else
+      (send-to-renderer "notification" {:type "error"
+                                        :payload (str "Unimplemented x-callback-url action: `"
+                                                      action
+                                                      "`.")}))))
+
 (defn logseq-url-handler
   [^js win parsed-url]
   (let [url-host (.-host parsed-url)] ;; return "" when no pathname provided
@@ -64,6 +80,9 @@
       (= "auth-callback" url-host)
       (send-to-renderer win "loginCallback" (.get (.-searchParams parsed-url) "code"))
 
+      (= "x-callback-url" url-host)
+      (x-callback-url-handler parsed-url)
+
       ;; identifier of graph in local
       (= "graph" url-host)
       (local-url-handler win parsed-url false)

+ 3 - 1
src/electron/electron/window.cljs

@@ -103,7 +103,9 @@
 (defn graph-has-other-windows? [win dir]
   (let [windows (get-graph-all-windows dir)]
         ;; windows (filter #(.isVisible %) windows) ;; for mac .hide windows. such windows should also included
-    (boolean (some (fn [^js window] (not= (.-id win) (.-id window))) windows))))
+    (boolean (some (fn [^js window] (and (not (.isDestroyed window))
+                                         (not= (.-id win) (.-id window))))
+                   windows))))
 
 (defn- open-default-app!
   [url default-open]

+ 26 - 1
src/main/electron/listener.cljs

@@ -1,9 +1,12 @@
 (ns electron.listener
   (:require [frontend.state :as state]
             [frontend.context.i18n :refer [t]]
+            [frontend.date :as date]
             [frontend.handler.route :as route-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.config :as config]
+            [clojure.string :as string]
             [cljs-bean.core :as bean]
             [frontend.fs.watcher-handler :as watcher-handler]
             [frontend.fs.sync :as sync]
@@ -34,7 +37,7 @@
    (fn [_req]
      (persist-dbs!))))
 
-(defn listen-to-electron!
+(defn ^:large-vars/cleanup-todo listen-to-electron!
   []
   ;; TODO: move "file-watcher" to electron.ipc.channels
   (js/window.apis.on "file-watcher"
@@ -125,6 +128,28 @@
                      (fn [code]
                        (user/login-callback code)))
 
+  (js/window.apis.on "quickCapture"
+                     (fn [args]
+                       (let [{:keys [url title content]} (bean/->clj args)
+                             page (or (state/get-current-page)
+                                      (string/lower-case (date/journal-name)))
+                             format (db/get-page-format page)
+                             time (date/get-current-time)
+                             text (or (and content (not-empty (string/trim content))) "")
+                             link (if (not-empty title) (config/link-format format title url) url)
+                             template (get-in (state/get-config)
+                                              [:quick-capture-templates :text]
+                                              "**{time}** [[quick capture]]: {text} {url}")
+                             content (-> template
+                                         (string/replace "{time}" time)
+                                         (string/replace "{url}" link)
+                                         (string/replace "{text}" text))]
+                         (if (and (state/get-edit-block) (state/editing?))
+                           (editor-handler/insert content)
+                           (editor-handler/api-insert-new-block! content {:page page
+                                                                          :edit-block? false
+                                                                          :replace-empty-target? true})))))
+
   (js/window.apis.on "openNewWindowOfGraph"
                      ;; Handle open new window in renderer, until the destination graph doesn't rely on setting local storage
                      ;; No db cache persisting ensured. Should be handled by the caller

+ 46 - 32
src/main/frontend/components/block.cljs

@@ -33,6 +33,7 @@
             [frontend.extensions.zotero :as zotero]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
+            [frontend.fs :as fs]
             [frontend.handler.block :as block-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.dnd :as dnd]
@@ -48,20 +49,20 @@
             [frontend.security :as security]
             [frontend.state :as state]
             [frontend.template :as template]
-            [logseq.graph-parser.text :as text]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.clock :as clock]
             [frontend.util.drawer :as drawer]
-            [frontend.util.text :as text-util]
             [frontend.util.property :as property]
-            [logseq.graph-parser.config :as gp-config]
-            [logseq.graph-parser.util :as gp-util]
-            [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.block :as gp-block]
+            [frontend.util.text :as text-util]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
+            [logseq.graph-parser.block :as gp-block]
+            [logseq.graph-parser.config :as gp-config]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.util :as gp-util]
             [medley.core :as medley]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
@@ -271,12 +272,15 @@
 
     (when @src
       (let [ext (keyword (util/get-file-ext @src))
+            repo (state/get-current-repo)
+            repo-dir (config/get-repo-dir repo)
+            path (str repo-dir href)
             share-fn (fn [event]
                        (util/stop event)
                        (when (mobile-util/native-platform?)
-                         (p/let [url (str (config/get-repo-dir (state/get-current-repo)) href)]
-                           (.share Share #js {:url url
-                                              :title "Open file with your favorite app"}))))]
+                         (.share Share #js {:url path
+                                            :title "Open file with your favorite app"})))]
+
         (cond
           (contains? config/audio-formats ext)
           (audio-cp @src)
@@ -284,13 +288,20 @@
           (contains? (gp-config/img-formats) ext)
           (resizable-image config title @src metadata full_text true)
 
+          (contains? (gp-config/text-formats) ext)
+          [:a.asset-ref.is-plaintext {:href (rfe/href :file {:path path})
+                                      :on-click (fn [_event]
+                                                  (p/let [result (fs/read-file repo-dir path)]
+                                                    (db/set-file-content! repo path result )))}
+           title]
+
           (= ext :pdf)
           [:a.asset-ref.is-pdf {:href @src
                                 :on-click share-fn}
            title]
 
           :else
-          [:a.asset-ref.is-doc {:ref @src
+          [:a.asset-ref.is-doc {:href @src
                                 :on-click share-fn}
            title])))))
 
@@ -605,13 +616,10 @@
         nested-link? (:nested-link? config)
         contents-page? (= "contents" (string/lower-case (str (:id config))))
         block-uuid (:block/uuid config)]
-    (cond
-      (string/ends-with? s ".excalidraw")
+    (if (string/ends-with? s ".excalidraw")
       [:div.draw {:on-click (fn [e]
                               (.stopPropagation e))}
        (excalidraw s block-uuid)]
-
-      :else
       [:span.page-reference
        {:data-ref s}
        (when (and (or show-brackets? nested-link?)
@@ -1142,7 +1150,7 @@
       (when-not (string/blank? id)
         (let [width (min (- (util/get-width) 96)
                          560)
-              height (int (* width (/ 315 560)))]
+              height (int (* width (/ 360 560)))]
           [:iframe
            {:allowfullscreen true
             :framespacing "0"
@@ -1156,10 +1164,7 @@
 (defn- macro-video-cp
   [_config arguments]
   (when-let [url (first arguments)]
-    (let [width (min (- (util/get-width) 96)
-                     560)
-          height (int (* width (/ 315 560)))
-          results (text-util/get-matched-video url)
+    (let [results (text-util/get-matched-video url)
           src (match results
                      [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
                      (if (= (count id) 11) ["youtube-player" id] url)
@@ -1173,8 +1178,10 @@
                      [_ _ _ (_ :guard #(string/ends-with? % "vimeo.com")) _ id _]
                      (str "https://player.vimeo.com/video/" id)
 
-                     [_ _ _ "bilibili.com" _ id _]
-                     (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
+                     [_ _ _ "bilibili.com" _ id & query]
+                     (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1"
+                          (when-let [page (second query)]
+                            (str "&page=" page)))
 
                      :else
                      url)]
@@ -1182,16 +1189,20 @@
                (= (first src) "youtube-player"))
         (youtube/youtube-video (last src))
         (when src
-          [:iframe
-           {:allowfullscreen true
-            :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
-            :framespacing "0"
-            :frameborder "no"
-            :border "0"
-            :scrolling "no"
-            :src src
-            :width width
-            :height height}])))))
+          (let [width (min (- (util/get-width) 96) 560)
+                height (int (* width (/ (if (string/includes? src "player.bilibili.com")
+                                          360 315)
+                                        560)))]
+            [:iframe
+             {:allow-full-screen true
+              :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+              :framespacing "0"
+              :frame-border "no"
+              :border "0"
+              :scrolling "no"
+              :src src
+              :width width
+              :height height}]))))))
 
 (defn- macro-else-cp
   [name config arguments]
@@ -1931,7 +1942,10 @@
         (when-not (target-forbidden-edit? target)
           (cond
             (and shift? (state/get-selection-start-block-or-first))
-            (editor-handler/highlight-selection-area! block-id)
+            (do
+              (util/stop e)
+              (util/clear-selection!)
+              (editor-handler/highlight-selection-area! block-id))
 
             shift?
             (util/clear-selection!)

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

@@ -335,6 +335,7 @@
   min-height: 24px;
   padding: 2px 0;
   border-bottom: 1px solid transparent;
+  transition: background-color 0.3s cubic-bezier(0.16, 1, 0.3, 1);
 
   &.selected {
     border-bottom-color: var(--ls-primary-background-color);

+ 2 - 0
src/main/frontend/components/external.cljs

@@ -1,3 +1,5 @@
+;; deprecated by the onboarding import panel frontend.components.onboarding.setups
+
 (ns frontend.components.external
   (:require [rum.core :as rum]
             [goog.object :as gobj]

+ 11 - 8
src/main/frontend/components/onboarding.cljs

@@ -23,25 +23,28 @@
                          [:span.mr-1 (t :help/shortcuts)]
                          (ui/icon "command" {:style {:font-size 20}})]]]
                       [(t :help/docs) "https://docs.logseq.com/"]
+                      [(t :help/start) "https://docs.logseq.com/#/page/tutorial"]
                       ["FAQ" "https://docs.logseq.com/#/page/faq"]]}
-
-          {:title "About"
-           :children [[(t :help/start) "https://docs.logseq.com/#/page/getting%20started"]
-                      [(t :help/about) "https://logseq.com/blog/about"]]}
+          
+          {:title "Community"
+           :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
+                      [(t :help/blog) "https://blog.logseq.com"]
+                      [discord-with-icon "https://discord.gg/KpN4eHY"]]}
 
           {:title "Development"
            :children [[(t :help/roadmap) "https://trello.com/b/8txSM12G/roadmap"]
                       [(t :help/bug) "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"]
                       [(t :help/feature) "https://github.com/logseq/logseq/issues/new?assignees=&labels=&template=feature_request.md&title="]
                       [(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
+          
+          {:title "About"
+           :children [[(t :help/about) "https://logseq.com/blog/about"]]}
 
           {:title "Terms"
            :children [[(t :help/privacy) "https://logseq.com/blog/privacy-policy"]
-                      [(t :help/terms) "https://logseq.com/blog/terms"]]}
+                      [(t :help/terms) "https://logseq.com/blog/terms"]]}]]
 
-          {:title "Community"
-           :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
-                      [discord-with-icon "https://discord.gg/KpN4eHY"]]}]]
+          
 
      (map (fn [sublist]
             [[:p.mt-4.mb-1 [:b (:title sublist)]]

+ 95 - 41
src/main/frontend/components/onboarding/setups.cljs

@@ -127,54 +127,125 @@
               [:small.opacity-50 label]]]))]]])))
 
 (defonce *roam-importing? (atom nil))
+(defonce *lsq-importing? (atom nil))
 (defonce *opml-importing? (atom nil))
 (defonce *opml-imported-pages (atom nil))
 
+(defn- finished-cb
+  []
+  (notification/show! "Import finished!" :success)
+  (route-handler/redirect-to-home!))
+
+(defn- roam-import-handler
+  [e]
+  (let [file      (first (array-seq (.-files (.-target e))))
+        file-name (gobj/get file "name")]
+    (if (string/ends-with? file-name ".json")
+      (do
+        (reset! *roam-importing? true)
+        (let [reader (js/FileReader.)]
+          (set! (.-onload reader)
+                (fn [e]
+                  (let [text (.. e -target -result)]
+                    (external-handler/import-from-roam-json! text
+                                                             #(do (reset! *roam-importing? false) (finished-cb))))))
+          (.readAsText reader file)))
+      (notification/show! "Please choose a JSON file."
+                          :error))))
+
+(defn- lsq-import-handler
+  [e]
+  (let [file      (first (array-seq (.-files (.-target e))))
+        file-name (some-> (gobj/get file "name")
+                          (string/lower-case))]
+    (cond (string/ends-with? file-name ".edn")
+          (do
+            (reset! *lsq-importing? true)
+            (let [reader (js/FileReader.)]
+              (set! (.-onload reader)
+                    (fn [e]
+                      (let [text (.. e -target -result)]
+                        (external-handler/import-from-edn! text
+                                                           #(do (reset! *lsq-importing? false) (finished-cb))))))
+              (.readAsText reader file)))
+
+          (string/ends-with? file-name ".json")
+          (do
+            (reset! *lsq-importing? true)
+            (let [reader (js/FileReader.)]
+              (set! (.-onload reader)
+                    (fn [e]
+                      (let [text (.. e -target -result)]
+                        (external-handler/import-from-json! text
+                                                            #(do (reset! *lsq-importing? false) (finished-cb))))))
+              (.readAsText reader file)))
+
+          :else
+          (notification/show! "Please choose an EDN or a JSON file."
+                              :error))))
+
+(defn- opml-import-handler
+  [e]
+  (let [file      (first (array-seq (.-files (.-target e))))
+        file-name (gobj/get file "name")]
+    (if (string/ends-with? file-name ".opml")
+      (do
+        (reset! *opml-importing? true)
+        (let [reader (js/FileReader.)]
+          (set! (.-onload reader)
+                (fn [e]
+                  (let [text (.. e -target -result)]
+                    (external-handler/import-from-opml! text
+                                                        (fn [pages]
+                                                          (reset! *opml-imported-pages pages)
+                                                          (reset! *opml-importing? false)
+                                                          (finished-cb))))))
+          (.readAsText reader file)))
+      (notification/show! "Please choose a OPML file."
+                          :error))))
+
 (rum/defc importer < rum/reactive
   [{:keys [query-params]}]
   (let [roam-importing? (rum/react *roam-importing?)
+        lsq-importing?  (rum/react *lsq-importing?)
         opml-importing? (rum/react *opml-importing?)
-        finished-cb     (fn []
-                          (notification/show! "Finished!" :success)
-                          (route-handler/redirect-to-home!))]
+        importing?      (or roam-importing? lsq-importing? opml-importing?)]
 
     (setups-container
      :importer
      [:article.flex.flex-col.items-center.importer.py-16.px-8
       [:section.c.text-center
        [:h1 "Do you already have notes that you want to import?"]
-       [:h2 "If they are in a JSON or Markdown format Logseq can work with them."]]
+       [:h2 "If they are in a JSON, EDN or Markdown format Logseq can work with them."]]
       [:section.d.md:flex
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled (or roam-importing? opml-importing?)}
+        {:disabled importing?}
         [:span.as-flex-center [:i (svg/roam-research 28)]]
         [:div.flex.flex-col
          (if roam-importing?
            (ui/loading "Importing ...")
-           [
-            [:strong "RoamResearch"]
+           [[:strong "RoamResearch"]
             [:small "Import a JSON Export of your Roam graph"]])]
         [:input.absolute.hidden
          {:id        "import-roam"
           :type      "file"
-          :on-change (fn [e]
-                       (let [file      (first (array-seq (.-files (.-target e))))
-                             file-name (gobj/get file "name")]
-                         (if (string/ends-with? file-name ".json")
-                           (do
-                             (reset! *roam-importing? true)
-                             (let [reader (js/FileReader.)]
-                               (set! (.-onload reader)
-                                     (fn [e]
-                                       (let [text (.. e -target -result)]
-                                         (external-handler/import-from-roam-json! text
-                                                                                  #(do (reset! *roam-importing? false) (finished-cb))))))
-                               (.readAsText reader file)))
-                           (notification/show! "Please choose a JSON file."
-                                               :error))))}]]
+          :on-change roam-import-handler}]]
+
+       [:label.action-input.flex.items-center.mx-2.my-2
+        {:disabled importing?}
+        [:span.as-flex-center [:i (svg/logo 28)]]
+        [:span.flex.flex-col
+         (if lsq-importing?
+           (ui/loading "Importing ...")
+           [[:strong "EDN / JSON"]
+            [:small "Import an EDN or a JSON Export of your Logseq graph"]])]
+        [:input.absolute.hidden
+         {:id        "import-lsq"
+          :type      "file"
+          :on-change lsq-import-handler}]]
 
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled (or roam-importing? opml-importing?)}
+        {:disabled importing?}
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.flex.flex-col
          (if opml-importing?
@@ -185,24 +256,7 @@
         [:input.absolute.hidden
          {:id        "import-opml"
           :type      "file"
-          :on-change (fn [e]
-                       (let [file      (first (array-seq (.-files (.-target e))))
-                             file-name (gobj/get file "name")]
-                         (if (string/ends-with? file-name ".opml")
-                           (do
-                             (reset! *opml-importing? true)
-                             (let [reader (js/FileReader.)]
-                               (set! (.-onload reader)
-                                     (fn [e]
-                                       (let [text (.. e -target -result)]
-                                         (external-handler/import-from-opml! text
-                                                                             (fn [pages]
-                                                                               (reset! *opml-imported-pages pages)
-                                                                               (reset! *opml-importing? false)
-                                                                               (finished-cb))))))
-                               (.readAsText reader file)))
-                           (notification/show! "Please choose a OPML file."
-                                               :error))))}]]]
+          :on-change opml-import-handler}]]]
 
       (when (= "picker" (:from query-params))
         [:section.e

+ 1 - 0
src/main/frontend/components/search.cljs

@@ -298,6 +298,7 @@
         [:a.text-sm.font-medium {:href (rfe/href :search {:q search-q})
                                  :on-click (fn []
                                              (when-not (string/blank? search-q)
+                                               (state/close-modal!)
                                                (search-handler/search (state/get-current-repo) search-q {:limit 1000
                                                                                                          :more? true})
                                                (search-handler/clear-search!)))}

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

@@ -619,7 +619,7 @@
      (when (and util/mac? (util/electron?)) (app-auto-update-row t))
      (usage-diagnostics-row t instrument-disabled?)
      (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
-     (when (util/electron?) (plugin-system-switcher-row))
+     (when (and (util/electron?) config/enable-plugins?) (plugin-system-switcher-row))
      (flashcards-switcher-row enable-flashcards?)
      (when (util/electron?) (https-user-agent-row https-agent-opts))
      (clear-cache-row t)

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

@@ -267,7 +267,7 @@
          {:class "whiteboard"
           :title "Whiteboards"
           :href  (rfe/href :whiteboards)
-          :icon  "files"})]]
+          :icon  "artboard"})]]
 
       (favorites t)
 
@@ -517,7 +517,8 @@
   [e]
   (state/hide-custom-context-menu!)
   (when-not (or (gobj/get e "shiftKey")
-                (util/meta-key? e))
+                (util/meta-key? e)
+                (state/get-edit-input-id))
     (editor-handler/clear-selection!)))
 
 (rum/defcs ^:large-vars/cleanup-todo sidebar <

+ 9 - 0
src/main/frontend/config.cljs

@@ -33,6 +33,15 @@
 (goog-define API-DOMAIN "api.logseq.com")
 (goog-define WS-URL "wss://og96xf1si7.execute-api.us-east-2.amazonaws.com/production?graphuuid=%s")
 
+;; feature flags
+(goog-define ENABLE-FILE-SYNC false)
+(defonce enable-file-sync? (or ENABLE-FILE-SYNC dev?)) ;; always enable file-sync when dev
+
+(goog-define ENABLE-PLUGINS true)
+(defonce enable-plugins? ENABLE-PLUGINS)
+
+(swap! state/state assoc :plugin/enabled enable-plugins?)
+
 ;; :TODO: How to do this?
 ;; (defonce desktop? ^boolean goog.DESKTOP)
 

+ 14 - 6
src/main/frontend/db.cljs

@@ -41,7 +41,7 @@
   delete-file-blocks! delete-page-blocks delete-files delete-pages-by-files
   filter-only-public-pages-and-blocks get-all-block-contents get-all-tagged-pages
   get-all-templates get-block-and-children get-block-by-uuid get-block-children sort-by-left
-  get-block-parent get-block-parents parents-collapsed? get-block-referenced-blocks
+  get-block-parent get-block-parents parents-collapsed? get-block-referenced-blocks get-all-referenced-blocks-uuid
   get-block-children-ids get-block-immediate-children get-block-page
   get-custom-css get-date-scheduled-or-deadlines
   get-file-blocks get-file-last-modified-at get-file get-file-page get-file-page-id file-exists?
@@ -52,7 +52,8 @@
   get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? page-alias-set pull-block
   set-file-last-modified-at! page-empty? page-exists? page-empty-or-dummy? get-alias-source-page
-  set-file-content! has-children? get-namespace-pages get-all-namespace-relation get-pages-by-name-partition]
+  set-file-content! has-children? get-namespace-pages get-all-namespace-relation get-pages-by-name-partition
+  get-original-name]
 
  [frontend.db.react
   get-current-page set-key-value
@@ -158,13 +159,13 @@
                 (assoc option
                        :listen-handler listen-and-persist!))))
 
-(defn restore-graph!
-  "Restore db from serialized db cache, and swap into the current db status"
-  [repo]
+(defn restore-graph-from-text!
+  "Swap db string into the current db status
+   stored: the text to restore from"
+  [repo stored]
   (p/let [db-name (datascript-db repo)
           db-conn (d/create-conn db-schema/schema)
           _ (swap! conns assoc db-name db-conn)
-          stored (db-persist/get-serialized-graph db-name)
           _ (when stored
               (let [stored-db (try (string->db stored)
                                    (catch js/Error _e
@@ -178,6 +179,13 @@
                 (conn/reset-conn! db-conn db)))]
     (d/transact! db-conn [{:schema/version db-schema/version}])))
 
+(defn restore-graph!
+  "Restore db from serialized db cache"
+  [repo]
+  (p/let [db-name (datascript-db repo)
+          stored (db-persist/get-serialized-graph db-name)]
+    (restore-graph-from-text! repo stored)))
+
 (defn restore!
   [{:keys [repos]} _old-db-schema restore-config-handler]
   (let [repo (or (state/get-current-repo) (:url (first repos)))]

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

@@ -71,6 +71,11 @@
        react
        first))))
 
+(defn get-original-name
+  [page-entity]
+  (or (:block/original-name page-entity)
+      (:block/name page-entity)))
+
 (defn get-tag-pages
   [repo tag-name]
   (when tag-name
@@ -1364,6 +1369,16 @@
          (reset! blocks-count-cache n)
          n)))))
 
+(defn get-all-referenced-blocks-uuid
+  "Get all uuids of blocks with any back link exists."
+  []
+  (when-let [db (conn/get-db)]
+    (d/q '[:find [?refed-uuid ...]
+           :where
+           ;; ?referee-b is block with ref towards ?refed-b
+           [?refed-b   :block/uuid ?refed-uuid]
+           [?referee-b :block/refs ?refed-b]] db)))
+
 ;; block/uuid and block/content
 (defn get-all-block-contents
   []

+ 41 - 3
src/main/frontend/dicts.cljc

@@ -2206,7 +2206,10 @@
            :page/file-sync-versions "Versões da página"
            :plugin/not-installed "Não instalado"
            :tutorial/dummy-notes "dummy-notes-en.md"
-           :tutorial/text "tutorial-en.md"}
+           :tutorial/text "tutorial-en.md"
+           :settings-page/edit-export-css "Editar export.css"
+           :settings-page/enable-flashcards "Flashcards"
+           :settings-page/export-theme "Exportar Tema"}
 
    :pt-PT {:on-boarding/demo-graph "Isto é um grafo de demonstração, nenhuma mudança será guardada até abrir uma pasta local."
            :on-boarding/add-graph "Adicionar grafo"
@@ -2500,7 +2503,39 @@
         :select.graph/add-graph "Sim, adicionar outro grafo"
 
         :file-sync/other-user-graph "O gráfico local atual é obrigado ao gráfico remoto de outro usuário. Portanto, não consigo iniciar a sincronização."
-        :file-sync/graph-deleted "O gráfico remoto atual foi excluído"}
+        :file-sync/graph-deleted "O gráfico remoto atual foi excluído"
+        :export-graph "Exportar gráfico"
+        :export-page "Exportar Página"
+        :host "Host"
+        :port "Port"
+        :re-index-discard-unsaved-changes-warning "A reindexação descartará o gráfico atual e processará todos os arquivos novamente conforme estão armazenados no disco. Você perderá as alterações não salvas e pode demorar um pouco. Continuar?"
+        :re-index-multiple-windows-warning "Você precisa fechar as outras janelas antes de reindexar este gráfico."
+        :save "Salvar"
+        :settings-of-plugins "Configurações dos plugins"
+        :sync-from-local-changes-detected "Atualizar detecta e processa arquivos modificados em seu disco e que são diferentes do conteúdo atual da página do Logseq. Continuar?"
+        :type "Tipo"
+        :content/copy-block-emebed "Copiar bloco para incorporar"
+
+        :graph/persist "O Logseq está sincronizando seu status interno, aguarde alguns segundos."
+        :graph/persist-error "Falha na sincronização do status interno."
+        :graph/save "Salvando..."
+        :graph/save-success "Salvo com sucesso"
+        :graph/save-error "Falha ao salvar"
+
+        :page/copy-page-url "Copiar URL da página"
+        :page/file-sync-versions "Versões da Página"
+        :page/open-backup-directory "Abra a listagem de backups de página"
+        :page/unfavorite "Desfavoritar página"
+        :plugin/not-installed "Não instalado"
+        :search/page-names "Procurar nome da página"
+        :settings-page/auto-updater "Auto atualizar"
+        :tutorial/dummy-notes "dummy-notes-en.md"
+        :tutorial/text "tutorial-en.md"
+        :settings-page/edit-export-css "Editar export.css"
+        :settings-page/enable-flashcards "Flashcards"
+        :settings-page/export-theme "Exportar tema"
+        :settings-page/network-proxy "Proxy de rede"
+        :settings-page/plugin-system "Sistema de plugins"}
 
    :ru {:on-boarding/demo-graph "Это демонстрационный граф, изменения не будут сохранены, пока вы не откроете локальный файл."
         :on-boarding/add-graph "Добавить новый граф"
@@ -3426,7 +3461,10 @@
 
         :file-sync/other-user-graph "Il grafo locale attuale è associato al grafo remoto di un altro utente. Non è quindi possibile avviare la sincronizzazione."
         :file-sync/graph-deleted "Il grafo attuale è stato eliminato"
-        :settings-page/enable-encryption "Crittografia"}
+        :settings-page/enable-encryption "Crittografia"
+        :settings-page/edit-export-css "Modificare export.css"
+        :settings-page/enable-flashcards "Flashcard"
+        :settings-page/export-theme "Esporta tema"}
 
    :tr {:tutorial/text #?(:cljs (rc/inline "tutorial-tr.md")
                                 :default "tutorial-tr.md")

+ 1 - 1
src/main/frontend/extensions/code.css

@@ -31,7 +31,7 @@
 
   &-calc {
     @apply text-sm;
-    padding: 0.25em;
+    padding: 23px 0.25em 0.25em 0.25em;
     width: max-content;
     text-align: right;
 

+ 4 - 19
src/main/frontend/extensions/html_parser.cljs

@@ -17,22 +17,10 @@
 (defn- export-hiccup
   [hiccup]
   (util/format "#+BEGIN_EXPORT hiccup\n%s\n#+END_EXPORT"
-
                (str (hiccup-without-style hiccup))))
 
-(def allowed-tags
-  #{:address, :article, :aside, :footer, :header,
-    :h1, :h2, :h3, :h4, :h5, :h6, :hgroup,
-    :main, :nav, :section,
-    :blockquote, :dd, :div, :dl, :dt, :figcaption, :figure,
-    :hr, :li, :ol, :p, :pre, :ul,
-    :a, :abbr, :b, :bdi, :bdo, :br, :cite, :code, :data, :dfn,
-    :em, :i, :kbd, :mark, :q,
-    :rb, :rp, :rt, :rtc, :ruby,
-    :s, :samp, :small, :span, :strong, :sub, :sup, :time, :u, :var, :wbr,
-    :caption, :col, :colgroup, :table, :tbody, :td, :tfoot, :th,
-    :thead, :tr
-    :body :html})
+(def denied-tags
+  #{:script :base :head :link :meta :style :title :comment :xml :svg :frame :frameset :embed :object :canvas :applet})
 
 (defn ^:large-vars/cleanup-todo hiccup->doc-inner
   [format hiccup opts]
@@ -91,10 +79,7 @@
                                       (if (string? pattern) pattern (apply str (reverse pattern)))))))
         wrapper (fn [tag content]
                   (let [content (cond
-                                  (not (contains? allowed-tags tag))
-                                  nil
-
-                                  (contains? #{:comment :head :style :xml} tag)
+                                  (contains? denied-tags tag)
                                   nil
 
                                   (and (= tag :p) (:in-table? opts))
@@ -233,7 +218,7 @@
                  (for [x hiccup]
                    (single-hiccup-transform x))
                  (single-hiccup-transform hiccup))]
-    (apply str result)))
+    (string/replace (apply str result) #"\n\n+" "\n\n")))
 
 (defn hiccup->doc
   [format hiccup]

+ 0 - 6
src/main/frontend/fs.cljs

@@ -67,12 +67,6 @@
     (when (= fs bfs-record)
       (protocol/rmdir! fs dir))))
 
-(defn delete-file!
-  [repo dir path opts]
-  (let [fs-record (get-fs dir)]
-    (when (= fs-record mobile-record)
-      (protocol/delete-file! fs-record repo dir path opts))))
-
 (defn write-file!
   [repo dir path content opts]
   (when content

+ 55 - 23
src/main/frontend/fs/capacitor_fs.cljs

@@ -2,6 +2,7 @@
   (:require ["@capacitor/filesystem" :refer [Encoding Filesystem]]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
+            [frontend.config :as config]
             [frontend.db :as db]
             [frontend.encrypt :as encrypt]
             [frontend.fs.protocol :as protocol]
@@ -107,6 +108,33 @@
         (= (string/trim decrypted-content) (string/trim db-content)))
       (p/resolved (= (string/trim disk-content) (string/trim db-content))))))
 
+(def backup-dir "logseq/bak")
+(defn- get-backup-dir
+  [repo-dir path ext]
+  (let [relative-path (-> (string/replace path repo-dir "")
+                          (string/replace (str "." ext) ""))]
+    (str repo-dir backup-dir "/" relative-path)))
+
+(defn- truncate-old-versioned-files!
+  "reserve the latest 3 version files"
+  [dir]
+  (p/let [files (readdir dir)
+          files (js->clj files :keywordize-keys true)
+          old-versioned-files (drop 3 (reverse (sort-by :mtime files)))]
+    (mapv (fn [file]
+            (.deleteFile Filesystem (clj->js {:path (js/encodeURI (:uri file))})))
+          old-versioned-files)))
+
+(defn backup-file
+  [repo-dir path content ext]
+  (let [backup-dir (get-backup-dir repo-dir path ext)
+        new-path (str backup-dir "/" (string/replace (.toISOString (js/Date.)) ":" "_") "." ext)]
+    (.writeFile Filesystem (clj->js {:data content
+                                     :path new-path
+                                     :encoding (.-UTF8 Encoding)
+                                     :recursive true}))
+    (truncate-old-versioned-files! backup-dir)))
+
 (defn- write-file-impl!
   [_this repo _dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
   (if skip-compare?
@@ -129,6 +157,7 @@
                                         (js/console.error error)
                                         nil)))
             disk-content (or disk-content "")
+            repo-dir (config/get-local-dir repo)
             ext (string/lower-case (util/get-file-ext path))
             db-content (or old-content (db/get-file repo (js/decodeURI path)) "")
             contents-matched? (contents-matched? disk-content db-content)
@@ -148,7 +177,12 @@
          (p/let [result (.writeFile Filesystem (clj->js {:path path
                                                          :data content
                                                          :encoding (.-UTF8 Encoding)
-                                                         :recursive true}))]
+                                                         :recursive true}))
+                 mtime (-> (js->clj stat :keywordize-keys true)
+                           :mtime)]
+           (when-not contents-matched?
+             (backup-file repo-dir path disk-content ext))
+           (db/set-file-last-modified-at! repo path mtime)
            (p/let [content (if (encrypt/encrypted-db? (state/get-current-repo))
                              (encrypt/decrypt content)
                              content)]
@@ -165,11 +199,14 @@
   (let [[dir path] (map #(some-> %
                                  js/decodeURI)
                         [dir path])
-        dir (string/replace dir #"/+$" "")
+        dir (some-> dir (string/replace #"/+$" ""))
         path (some-> path (string/replace #"^/+" ""))
         path (cond (nil? path)
                    dir
 
+                   (nil? dir)
+                   path
+
                    (string/starts-with? path dir)
                    path
 
@@ -204,13 +241,12 @@
 (defrecord Capacitorfs []
   protocol/Fs
   (mkdir! [_this dir]
-    (p/let [result (.mkdir Filesystem
-                           (clj->js
-                            {:path dir
-                             ;; :directory (.-ExternalStorage Directory)
-                             }))]
-      (js/console.log result)
-      result))
+    (-> (.mkdir Filesystem
+                (clj->js
+                 {:path dir}))
+        (p/catch (fn [error]
+                   (log/error :mkdir! {:path dir
+                                       :error error})))))
   (mkdir-recur! [_this dir]
     (p/let [result (.mkdir Filesystem
                            (clj->js
@@ -221,8 +257,16 @@
       result))
   (readdir [_this dir]                  ; recursive
     (readdir dir))
-  (unlink! [_this _repo _path _opts]
-    nil)
+  (unlink! [this repo path _opts]
+    (p/let [path (get-file-path nil path)
+            repo-dir (config/get-local-dir repo)
+            recycle-dir (str repo-dir config/app-name "/.recycle")
+            file-name (-> (string/replace path repo-dir "")
+                          (string/replace "/" "_")
+                          (string/replace "\\" "_"))
+            new-path (str recycle-dir "/" file-name)]
+      (protocol/mkdir! this recycle-dir)
+      (protocol/rename! this repo path new-path)))
   (rmdir! [_this _dir]
     ;; Too dangerious!!! We'll never implement this.
     nil)
@@ -236,18 +280,6 @@
          content)
        (p/catch (fn [error]
                   (log/error :read-file-failed error))))))
-  (delete-file! [_this repo dir path {:keys [ok-handler error-handler]}]
-    (let [path (get-file-path dir path)]
-      (p/catch
-       (p/let [result (.deleteFile Filesystem
-                                   (clj->js
-                                    {:path path}))]
-         (when ok-handler
-           (ok-handler repo path result)))
-       (fn [error]
-         (if error-handler
-           (error-handler error)
-           (log/error :delete-file-failed error))))))
   (write-file! [this repo dir path content opts]
     (let [path (get-file-path dir path)]
       (p/let [stat (p/catch

+ 0 - 3
src/main/frontend/fs/nfs.cljs

@@ -90,7 +90,6 @@
   (readdir [_this dir]
     (let [prefix (str "handle/" dir)
           cached-files (keys @nfs-file-handles-cache)]
-      (println "cached-files" cached-files)
       (p/resolved
        (->> (filter #(string/starts-with? % (str prefix "/")) cached-files)
             (map (fn [path]
@@ -240,6 +239,4 @@
 
   ;; TODO:
   (watch-dir! [_this _dir]
-    nil)
-  (unwatch-dir! [_this dir]
     nil))

+ 0 - 1
src/main/frontend/fs/protocol.cljs

@@ -9,7 +9,6 @@
   (unlink! [this repo path opts])
   (rmdir! [this dir])
   (read-file [this dir path opts])
-  (delete-file! [this repo dir path opts])
   (write-file! [this repo dir path content opts])
   (rename! [this repo old-path new-path])
   (stat [this dir path])

+ 22 - 10
src/main/frontend/handler/block.cljs

@@ -150,22 +150,34 @@
 (def *swipe (atom nil))
 (def *touch-start (atom nil))
 
+(defn- target-disable-swipe?
+  [target]
+  (let [user-defined-tags (get-in (state/get-config)
+                                  [:mobile :gestures/disabled-in-block-with-tags])]
+    (or (.closest target ".dsl-query")
+        (.closest target ".drawer")
+        (.closest target ".draw-wrap")
+        (some #(.closest target (util/format "[data-refs-self*=%s]" %))
+              user-defined-tags))))
+
 (defn on-touch-start
   [event uuid]
-  (let [input (state/get-input)
+  (let [target (.-target event)
+        input (state/get-input)
         input-id (state/get-edit-input-id)
         selection-type (.-type (.getSelection js/document))]
     (reset! *touch-start (js/Date.now))
     (when-not (and input
                    (string/ends-with? input-id (str uuid)))
       (state/clear-edit!))
-    (when (not= selection-type "Range")
-      (when-let [touches (.-targetTouches event)]
-        (when (= (.-length touches) 1)
-          (let [touch (aget touches 0)
-                x (.-clientX touch)
-                y (.-clientY touch)]
-            (reset! *swipe {:x0 x :y0 y :xi x :yi y :tx x :ty y :direction nil})))))))
+    (when-not (target-disable-swipe? target)
+      (when (not= selection-type "Range")
+        (when-let [touches (.-targetTouches event)]
+          (when (= (.-length touches) 1)
+            (let [touch (aget touches 0)
+                  x (.-clientX touch)
+                  y (.-clientY touch)]
+              (reset! *swipe {:x0 x :y0 y :xi x :yi y :tx x :ty y :direction nil}))))))))
 
 (defn on-touch-move
   [event block uuid edit? *show-left-menu? *show-right-menu?]
@@ -197,8 +209,8 @@
               (let [{:keys [x0 y0]} @*swipe
                     dx (- tx x0)
                     dy (- ty y0)]
-                (when (and (< (. js/Math abs dy) 20)
-                           (> (. js/Math abs dx) 10))
+                (when (and (< (. js/Math abs dy) 30)
+                           (> (. js/Math abs dx) 30))
                   (let [left (gdom/getElement (str "block-left-menu-" uuid))
                         right (gdom/getElement (str "block-right-menu-" uuid))]
 

+ 33 - 15
src/main/frontend/handler/editor.cljs

@@ -885,6 +885,7 @@
                   content (property/remove-empty-properties content)
                   block {:block/uuid block-id
                          :block/properties properties
+                         :block/properties-order (keys properties)
                          :block/content content}]
               (outliner-core/save-block! block))))))
 
@@ -942,14 +943,17 @@
               (state/set-edit-content! input-id new-content)
               (save-block-if-changed! block new-content))))))))
 
-(defn- set-blocks-id!
+(defn set-blocks-id!
+  "Persist block uuid to file if the uuid is valid, and it's not persisted in file.
+   Accepts a list of uuids."
   [block-ids]
   (let [block-ids (remove nil? block-ids)
         col (map (fn [block-id]
-                   (let [block (db/entity [:block/uuid block-id])]
+                   (when-let [block (db/entity [:block/uuid block-id])]
                      (when-not (:block/pre-block? block)
                        [block-id :id (str block-id)])))
-                 block-ids)]
+                 block-ids)
+        col (remove nil? col)]
     (batch-set-block-property! col)))
 
 (defn copy-block-ref!
@@ -1915,10 +1919,13 @@
    0))
 
 (defn paste-blocks
+  "Given a vec of blocks, insert them into the target page.
+   keep-uuid?: if true, keep the uuid provided in the block structure."
   [blocks {:keys [content-update-fn
                   exclude-properties
                   target-block
-                  sibling?]
+                  sibling?
+                  keep-uuid?]
            :or {exclude-properties []}}]
   (let [editing-block (when-let [editing-block (state/get-edit-block)]
                         (some-> (db/pull (:db/id editing-block))
@@ -1945,14 +1952,16 @@
         (let [format (or (:block/format target-block) (state/get-preferred-format))
               blocks' (map (fn [block]
                              (paste-block-cleanup block page exclude-properties format content-update-fn))
-                        blocks)
+                           blocks)
               result (outliner-core/insert-blocks! blocks' target-block {:sibling? sibling?
                                                                          :outliner-op :paste
-                                                                         :replace-empty-target? true})]
+                                                                         :replace-empty-target? true
+                                                                         :keep-uuid? keep-uuid?})]
           (edit-last-block-after-inserted! result))))))
 
 (defn- block-tree->blocks
-  [tree-vec format]
+  "keep-uuid? - maintain the existing :uuid in tree vec"
+  [tree-vec format keep-uuid?]
   (->> (outliner-core/tree-vec-flatten tree-vec)
        (map (fn [block]
               (let [content (:content block)
@@ -1961,22 +1970,31 @@
                                   (property/insert-properties format content props))
                     ast (mldoc/->edn content* (gp-mldoc/default-config format))
                     blocks (block/extract-blocks ast content* true format)
-                    fst-block (first blocks)]
+                    fst-block (first blocks)
+                    fst-block (if (and keep-uuid? (uuid? (:uuid block)))
+                                (assoc fst-block :block/uuid (:uuid block))
+                                fst-block)]
                 (assert fst-block "fst-block shouldn't be nil")
                 (assoc fst-block :block/level (:block/level block)))))))
 
-(defn insert-block-tree-after-target
+(defn insert-block-tree
   "`tree-vec`: a vector of blocks.
-   Block element: {:content :properties :children [block-1, block-2, ...]}"
-  [target-block-id sibling? tree-vec format]
-  (let [blocks (block-tree->blocks tree-vec format)
-        target-block (db/pull target-block-id)
+   A block element: {:content :properties :children [block-1, block-2, ...]}"
+  [tree-vec format {:keys [target-block keep-uuid?] :as opts}]
+  (let [blocks (block-tree->blocks tree-vec format keep-uuid?)
         page-id (:db/id (:block/page target-block))
         blocks (gp-block/with-parent-and-left page-id blocks)]
     (paste-blocks
      blocks
-     {:target-block target-block
-      :sibling? sibling?})))
+     opts)))
+
+(defn insert-block-tree-after-target
+  "`tree-vec`: a vector of blocks.
+   A block element: {:content :properties :children [block-1, block-2, ...]}"
+  [target-block-id sibling? tree-vec format]
+  (insert-block-tree tree-vec format
+                     {:target-block (db/pull target-block-id)
+                      :sibling?     sibling?}))
 
 (defn insert-template!
   ([element-id db-id]

+ 118 - 1
src/main/frontend/handler/external.cljs

@@ -1,5 +1,7 @@
 (ns frontend.handler.external
-  (:require [frontend.external :as external]
+  (:require [clojure.edn :as edn]
+            [clojure.walk :as walk]
+            [frontend.external :as external]
             [frontend.handler.file :as file-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.state :as state]
@@ -13,9 +15,11 @@
             [logseq.graph-parser.date-time-util :as date-time-util]
             [frontend.handler.page :as page]
             [frontend.handler.editor :as editor]
+            [frontend.handler.notification :as notification]
             [frontend.util :as util]))
 
 (defn index-files!
+  "Create file structure, then parse into DB (client only)"
   [repo files finish-handler]
   (let [titles (->> files
                     (map :title)
@@ -95,3 +99,116 @@
          {:target-block target-block
           :sibling? sibling?})
         (finished-ok-handler [page-name])))))
+
+(defn create-page-with-exported-tree!
+  "Create page from the per page object generated in `export-repo-as-edn-v2!`
+   Return page-name (title)
+   Extension to `insert-block-tree-after-target`
+   :id       - page's uuid
+   :title    - page's title (original name)
+   :children - tree
+   "
+  [{:keys [uuid title children] :as tree}]
+  (let [has-children? (seq children)
+        page-format (some-> tree (:children) (first) (:format))]
+    (try (page/create! title {:redirect?  false
+                              :format     page-format
+                              :uuid       uuid})
+         (catch js/Error e
+           (notification/show! (str "Error happens when creating page " title ":\n"
+                                    e
+                                    "\nSkipped and continue the remaining import.") :error)))
+    (when has-children?
+      (let [page-block  (db/entity [:block/name (util/page-name-sanity-lc title)])
+            first-child (first (:block/_left page-block)) ]
+        ;; Missing support for per block format (or deprecated?)
+        (try (editor/insert-block-tree children page-format
+                                       {:target-block first-child
+                                        :sibling?     true
+                                        :keep-uuid?   true})
+             (catch js/Error e
+               (notification/show! (str "Error happens when creating block content of page " title "\n"
+                                        e
+                                        "\nSkipped and continue the remaining import.") :error))))))
+  title)
+
+(defn- pre-transact-uuids
+  "Collect all uuids from page trees and write them to the db before hand."
+  [pages]
+  (let [uuids (map (fn [block]
+                     {:block/uuid (:uuid block)})
+                   (mapcat #(tree-seq map? :children %)
+                           pages))]
+    (db/transact! uuids)
+    pages))
+
+(defn- import-from-tree!
+  "Not rely on file system - backend compatible.
+   tree-translator-fn: translate exported tree structure to the desired tree for import"
+  [data tree-translator-fn]
+  (try (->> (:blocks data)
+            (map tree-translator-fn)
+            (pre-transact-uuids)
+            (mapv create-page-with-exported-tree!))
+       (editor/set-blocks-id! (db/get-all-referenced-blocks-uuid))
+       (catch js/Error e
+         (notification/show! (str "Error happens when importing:\n" e) :error))))
+
+(defn tree-vec-translate-edn
+  "Actions to do for loading edn tree structure.
+   1) Removes namespace `:block/` from all levels of the `tree-vec`
+   2) Rename all :block/page-name to :title
+   3) Rename all :block/id to :uuid"
+  ([tree-vec]
+   (let [kw-trans-fn #(-> %
+                          str
+                          (string/replace ":block/page-name" ":block/title")
+                          (string/replace ":block/id" ":block/uuid")
+                          (string/replace ":block/" "")
+                          keyword)
+         map-trans-fn (fn [acc k v]
+                        (assoc acc (kw-trans-fn k) v))
+         tree-trans-fn (fn [form]
+                         (if (and (map? form)
+                                  (:block/id form))
+                           (reduce-kv map-trans-fn {} form)
+                           form))]
+     (walk/postwalk tree-trans-fn tree-vec))))
+
+(defn import-from-edn!
+  [raw finished-ok-handler]
+  (import-from-tree! (edn/read-string raw) tree-vec-translate-edn)
+  (finished-ok-handler nil)) ;; it was designed to accept a list of imported page names but now deprecated
+
+(defn tree-vec-translate-json
+  "Actions to do for loading json tree structure.
+   1) Rename all :id to :uuid
+   2) Rename all :page-name to :title
+   3) Rename all :format \"markdown\" to :format `:markdown`"
+  ([tree-vec]
+   (let [kw-trans-fn #(-> %
+                          str
+                          (string/replace ":page-name" ":title")
+                          (string/replace ":id" ":uuid")
+                          (string/replace #"^:" "")
+                          keyword)
+         map-trans-fn (fn [acc k v]
+                        (cond (= :format k)
+                              (assoc acc (kw-trans-fn k) (keyword v))
+                              (= :id k)
+                              (assoc acc (kw-trans-fn k) (uuid v))
+                              :else
+                              (assoc acc (kw-trans-fn k) v)))
+         tree-trans-fn (fn [form]
+                         (if (and (map? form)
+                                  (:id form))
+                           (reduce-kv map-trans-fn {} form)
+                           form))]
+     (walk/postwalk tree-trans-fn tree-vec))))
+
+(defn import-from-json!
+  [raw finished-ok-handler]
+  (let [json     (js/JSON.parse raw)
+        clj-data (js->clj json :keywordize-keys true)]
+    (import-from-tree! clj-data tree-vec-translate-json))
+  (finished-ok-handler nil)) ;; it was designed to accept a list of imported page names but now deprecated

+ 1 - 1
src/main/frontend/handler/file_sync.cljs

@@ -15,7 +15,7 @@
             [frontend.handler.user :as user]
             [frontend.fs :as fs]))
 
-(def hiding-login&file-sync (not config/dev?))
+(def hiding-login&file-sync (not config/enable-file-sync?))
 (def refresh-file-sync-component (atom false))
 
 (defn graph-txid-exists?

+ 2 - 5
src/main/frontend/handler/graph.cljs

@@ -90,8 +90,7 @@
             namespaces (db/get-all-namespace-relation repo)
             tags (set (map second tagged-pages))
             full-pages (db/get-all-pages repo)
-            get-original-name (fn [p] (or (:block/original-name p) (:block/name p)))
-            all-pages (map get-original-name full-pages)
+            all-pages (map db/get-original-name full-pages)
             page-name->original-name (zipmap (map :block/name full-pages) all-pages)
             pages-after-journal-filter (if-not journal?
                                          (remove :block/journal? full-pages)
@@ -164,9 +163,7 @@
                        (distinct))
             nodes (build-nodes dark? page links (set tags) nodes namespaces)
             full-pages (db/get-all-pages repo)
-            get-original-name (fn [p] (or (:block/original-name p)
-                                         (:block/name p)))
-            all-pages (map get-original-name full-pages)
+            all-pages (map db/get-original-name full-pages)
             page-name->original-name (zipmap (map :block/name full-pages) all-pages)]
         (normalize-page-name
          {:nodes nodes

+ 18 - 16
src/main/frontend/handler/page.cljs

@@ -116,27 +116,35 @@
         [page]))))
 
 (defn create!
+  "Create page.
+   :redirect?           - when true, redirect to the created page, otherwise return sanitized page name.
+   :split-namespace?    - when true, split hierarchical namespace into levels.
+   :create-first-block? - when true, create an empty block if the page is empty.
+   :uuid                - when set, use this uuid instead of generating a new one."
   ([title]
    (create! title {}))
-  ([title {:keys [redirect? create-first-block? format properties split-namespace? journal?]
+  ([title {:keys [redirect? create-first-block? format properties split-namespace? journal? uuid]
            :or   {redirect?           true
                   create-first-block? true
                   format              nil
                   properties          nil
-                  split-namespace?    true}}]
-   (let [title (string/trim title)
-         title (gp-util/remove-boundary-slashes title)
-         page-name (util/page-name-sanity-lc title)
-         repo (state/get-current-repo)]
+                  split-namespace?    true
+                  uuid                nil}}]
+   (let [title      (string/trim title)
+         title      (gp-util/remove-boundary-slashes title)
+         page-name  (util/page-name-sanity-lc title)
+         repo       (state/get-current-repo)
+         with-uuid? (if (uuid? uuid) uuid true)] ;; FIXME: prettier validation
      (when (db/page-empty? repo page-name)
        (let [pages    (if split-namespace?
                         (gp-util/split-namespace-pages title)
                         [title])
              format   (or format (state/get-preferred-format))
              pages    (map (fn [page]
-                             (-> (block/page-name->map page true)
+                             ;; only apply uuid to the deepest hierarchy of page to create if provided.
+                             (-> (block/page-name->map page (if (= page title) with-uuid? true))
                                  (assoc :block/format format)))
-                        pages)
+                           pages)
              txs      (->> pages
                            ;; for namespace pages, only last page need properties
                            drop-last
@@ -167,14 +175,8 @@
     (when-not (string/blank? file-path)
       (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
       (when unlink-file?
-        (->
-         (p/let [_ (and (config/local-db? repo)
-                        (mobile-util/native-platform?)
-                        ;; TODO: @leizhe remove fs/delete-file! and use fs/unlink!
-                        (fs/delete-file! repo file-path file-path {}))
-                 _ (fs/unlink! repo (config/get-repo-path repo file-path) nil)])
-         (p/catch (fn [err]
-                    (js/console.error "error: " err))))))))
+        (-> (fs/unlink! repo (config/get-repo-path repo file-path) nil)
+            (p/catch (fn [error] (js/console.error error))))))))
 
 (defn- compute-new-file-path
   [old-path new-name]

+ 77 - 16
src/main/frontend/modules/shortcut/dicts.cljc

@@ -605,7 +605,62 @@
              :command.editor/backward-kill-word       "Apagar a palavra anterior"
              :command.editor/open-edit                "Editar bloco selecionado"
              :command.editor/delete-selection         "Eliminar blocos selecionados"
-             :command.editor/toggle-open-blocks       "Alternar blocos abertos (colapsar ou expandir todos)"}
+             :command.editor/toggle-open-blocks       "Alternar blocos abertos (colapsar ou expandir todos)"
+
+             :command.auto-complete/complete          "Auto-completar: Escolha o item selecionado"
+             :command.auto-complete/next              "Auto-completar: Selecione o próximo item"
+             :command.auto-complete/prev              "Auto-completar: Selecione o item anterior"
+             :command.auto-complete/shift-complete    "Auto-completar: Abra o item selecionado na barra lateral"
+             :command.auto-complete/open-link         "Auto-completar: Abra o item selecionado no navegador"
+             :command.cards/forgotten                 "Cartões: Esquecido"
+             :command.cards/next-card                 "Cartões: Próxima carta"
+             :command.cards/recall                    "Cartões: Demorar um pouco para lembrar"
+             :command.cards/remembered                "Cartões: Relembrado"
+             :command.cards/toggle-answers            "Cartões: mostrar/esconder as respostas/clozes"
+             :command.command/run                     "Execute o comando Git"
+             :command.command/toggle-favorite         "Adicionar aos/remover dos favoritos"
+             :command.command-palette/toggle          "Editar atalhos"
+             :command.date-picker/complete            "Escolher data: Escolha o dia selecionado"
+             :command.date-picker/next-day            "Escolher data: Selecione o próximo dia"
+             :command.date-picker/next-week           "Escolher data: Selecione a próxima semana"
+             :command.date-picker/prev-day            "Escolher data: Selecione o dia anterior"
+             :command.date-picker/prev-week           "Escolher data: Selecione a semana anterior"
+             :command.editor/copy-current-file        "Copiar o arquivo atual"
+             :command.editor/select-down              "Selecione o conteúdo abaixo"
+             :command.editor/select-up                "Selecione o conteúdo acima"
+             :command.editor/copy-embed               "Copiar uma incorporação do bloco, apontando para o bloco atual"
+             :command.editor/copy-text                "Copiar seleção como texto"
+             :command.pdf/close                       "Fechar visualização do PDF"
+             :command.editor/escape-editing           "Sair da edição"
+             :command.editor/insert-youtube-timestamp "Inserir timestamp do youtube"
+             :command.editor/paste-text-in-one-block-at-point "Colar texto em um bloco no ponto"
+             :command.editor/replace-block-reference-at-point "Substitua a referência do bloco pelo seu conteúdo no ponto"
+             :command.editor/strike-through           "Tachar"
+             :command.editor/open-file-in-default-app "Abra o arquivo no aplicativo padrão"
+             :command.editor/open-file-in-directory   "Abra o arquivo na pasta"
+             :command.go/all-pages                    "Ir para todas as páginas"
+             :command.go/backward                     "Voltar"
+             :command.go/flashcards                   "Trocar flashcards"
+             :command.go/forward                      "Avançar"
+             :command.go/graph-view                   "Ir para o gráfico"
+             :command.go/home                         "Volar para o inicio"
+             :command.go/keyboard-shortcuts           "Ir para os atalhos do teclado"
+             :command.go/next-journal                 "Ir ao proximo jornal"
+             :command.go/prev-journal                 "Ir ao jornal anterior"
+             :command.go/tomorrow                     "Ir para amanhã"
+             :command.graph/add                       "Adicionar um gráfico"
+             :command.graph/open                      "Selecionar gráfico para abrir"
+             :command.graph/remove                    "Remover um gráfico"
+             :command.pdf/next-page                   "Próxima página do atual pdf doc"
+             :command.pdf/previous-page               "Página anterior do atual pdf doc"
+             :command.sidebar/clear                   "Limpar tudo da barra lateral direita"
+             :command.sidebar/open-today-page         "Abrir a página de hoje na barra lateral direita"
+             :command.ui/select-theme-color           "Selecionar as cores do tema disponível"
+             :command.ui/toggle-cards                 "Trocar cartões"
+             :command.ui/toggle-left-sidebar          "Trocar barra lateral esquerda"
+             :command.graph/save                      "Salvar gráfico atual no computador"
+             :command.misc/copy                       "Copiar (copiar seleção ou referência do bloco)"
+             :command.ui/goto-plugins                 "Ir para o painel de plugins"}
 
    :pt-BR   {:shortcut.category/formatting            "Formatação"
              :shortcut.category/basics                "Básico"
@@ -674,18 +729,18 @@
              :command.auto-complete/next              "Auto-completar: Selecione o próximo item"
              :command.auto-complete/prev              "Auto-completar: Selecione o item anterior"
              :command.auto-complete/shift-complete    "Auto-completar: Abra o item selecionado na barra lateral"
-             :command.cards/forgotten                 "Cartas: Esquecido"
-             :command.cards/next-card                 "Cartas: Próxima carta"
-             :command.cards/recall                    "Cartas: Demorar um pouco para lembrar"
-             :command.cards/remembered                "Cartas: Relembrado"
-             :command.cards/toggle-answers            "Cartas: mostrar/esconder as respostas/clozes"
-             :command.command/toggle-favorite         "Adicionar para/remover dos favoritos"
+             :command.cards/forgotten                 "Cartões: Esquecido"
+             :command.cards/next-card                 "Cartões: Próxima carta"
+             :command.cards/recall                    "Cartões: Demorar um pouco para lembrar"
+             :command.cards/remembered                "Cartões: Relembrado"
+             :command.cards/toggle-answers            "Cartões: mostrar/esconder as respostas/clozes"
+             :command.command/toggle-favorite         "Adicionar aos/remover dos favoritos"
              :command.command-palette/toggle          "Editar atalhos"
-             :command.date-picker/complete            "Pegar data: Escolha o dia selecionado"
-             :command.date-picker/next-day            "Pegar data: Selecione o próximo dia"
-             :command.date-picker/next-week           "Pegar data: Selecione a próxima semana"
-             :command.date-picker/prev-day            "Pegar data: Selecione o dia anterior"
-             :command.date-picker/prev-week           "Pegar data: Selecione a semana anterior"
+             :command.date-picker/complete            "Escolher data: Escolha o dia selecionado"
+             :command.date-picker/next-day            "Escolher data: Selecione o próximo dia"
+             :command.date-picker/next-week           "Escolher data: Selecione a próxima semana"
+             :command.date-picker/prev-day            "Escolher data: Selecione o dia anterior"
+             :command.date-picker/prev-week           "Escolher data: Selecione a semana anterior"
              :command.editor/escape-editing           "Sair da edição"
              :command.editor/insert-youtube-timestamp "Inserir timestamp do youtube"
              :command.editor/paste-text-in-one-block-at-point "Colar texto em um bloco no ponto"
@@ -709,7 +764,7 @@
              :command.sidebar/clear                   "Limpar tudo da barra lateral direita"
              :command.sidebar/open-today-page         "Abrir a página de hoje na barra lateral direita"
              :command.ui/select-theme-color           "Selecionar as cores do tema disponível"
-             :command.ui/toggle-cards                 "Trocar cartas"
+             :command.ui/toggle-cards                 "Trocar cartões"
              :command.ui/toggle-left-sidebar          "Trocar barra lateral esquerda"
              :command.auto-complete/open-link         "Auto-completar: Abra o item selecionado no navegador"
              :command.command/run                     "Execute o comando Git"
@@ -721,9 +776,12 @@
              :command.ui/goto-plugins                 "Ir para o painel de plugins"
             ;;  :command.ui/open-new-window              "Abra uma nova janela"
              :command.editor/select-down              "Selecione o conteúdo abaixo"
-             :command.editor/select-up                "Selecione o conteúdo acima"}
+             :command.editor/select-up                "Selecione o conteúdo acima"
+             :command.editor/copy-embed               "Copiar uma incorporação do bloco, apontando para o bloco atual"
+             :command.editor/copy-text                "Copiar seleção como texto"
+             :command.pdf/close                       "Fechar visualização do PDF"}
 
-   :ja      {:shortcut.category/formatting                "フォーマット"
+   :ja      {:shortcut.category/formatting            "フォーマット"
              :shortcut.category/basics                "基本操作"
              :shortcut.category/navigating            "ナビゲーション"
              :shortcut.category/block-editing         "ブロック単位の編集"
@@ -953,7 +1011,10 @@
              :shortcut.category/block-command-editing "Modifica comandi blocco"
              :shortcut.category/block-selection       "Selezione blocco (premi Esc per uscire dalla selezione)"
              :shortcut.category/toggle                "Attiva/disattiva"
-             :shortcut.category/others                "Altri"}
+             :shortcut.category/others                "Altri"
+             :command.editor/copy-embed               "Copia un incorporamento di blocco che punta al blocco corrente"
+             :command.editor/copy-text                "Copia le selezioni come testo"
+             :command.pdf/close                       "Chiudi anteprima PDF"}
 
    
    :tr      {:shortcut.category/basics "Temel bilgiler"

+ 1 - 2
src/main/frontend/state.cljs

@@ -688,8 +688,7 @@
   (swap! state assoc
          :selection/mode false
          :selection/blocks nil
-         :selection/direction :down
-         :selection/start-block nil))
+         :selection/direction :down))
 
 (defn get-selection-blocks
   []

+ 15 - 7
src/main/frontend/util/marker.cljs

@@ -13,14 +13,22 @@
 (defn add-or-update-marker
   [content format marker]
   (let [[re-pattern new-line-re-pattern]
-        (if (= :org format)
-          [#"\*+\s" #"\n\*+\s"]
-          [#"#+\s" #"\n#+\s"])
+        (case format
+          :org
+          [#"^\*+\s" #"\n\*+\s"]
+
+          (:markdown :md)
+          [#"^#+\s" #"\n#+\s"]
+
+          ;; failback to markdown
+          [#"^#+\s" #"\n#+\s"])
+
         pos
         (if-let [matches (seq (util/re-pos new-line-re-pattern content))]
           (let [[start-pos content] (last matches)]
             (+ start-pos (count content)))
           (count (util/safe-re-find re-pattern content)))
+
         new-content
         (str (subs content 0 pos)
              (string/replace-first (subs content pos)
@@ -57,8 +65,8 @@
   Returns [new-content new-marker]."
   [content marker new-marker format preferred-workflow]
   (let [content    (string/triml content)
-        new-marker (or new-marker
-                       (cycle-marker-state (or marker
-                                               (last (util/safe-re-find (marker-pattern format) content))) ; Returns the last matching group (last vec)
-                                           preferred-workflow))]
+        marker     (or (not-empty marker)
+                       (last (util/safe-re-find (marker-pattern format) content))) ; Returns the last matching group (last vec)
+        new-marker (or (not-empty new-marker)
+                       (cycle-marker-state marker preferred-workflow))]
     [(add-or-update-marker content format new-marker) new-marker]))

+ 1 - 1
src/main/frontend/util/text.cljs

@@ -5,7 +5,7 @@
 
 (defonce between-re #"\(between ([^\)]+)\)")
 
-(def bilibili-regex #"^((?:https?:)?//)?((?:www).)?((?:bilibili.com))(/(?:video/)?)([\w-]+)(\S+)?$")
+(def bilibili-regex #"^((?:https?:)?//)?((?:www).)?((?:bilibili.com))(/(?:video/)?)([\w-]+)(\?p=(\d+))?(\S+)?$")
 (def loom-regex #"^((?:https?:)?//)?((?:www).)?((?:loom.com))(/(?:share/|embed/))([\w-]+)(\S+)?$")
 (def vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com))(/(?:video/)?)([\w-]+)(\S+)?$")
 (def youtube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be|y2u.be|youtube-nocookie.com))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)([\S^\?]+)?$")

+ 1 - 1
src/main/frontend/version.cljs

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.7.4")
+(defonce version "0.7.5")

+ 7 - 2
templates/config.edn

@@ -223,6 +223,11 @@
  ;; :mobile/photo
  ;; {:allow-editing? true}
 
+ ;; Mobile features options
+ ;; Gestures
+ ;; :mobile
+ ;; {:gestures/disabled-in-block-with-tags ["kanban"]}
+
  ;; Extra CodeMirror options
  ;; :editor/extra-codemirror-options {:keyMap "emacs" :lineWrapping true}
 
@@ -238,6 +243,6 @@
  ;; You can also reorder them, or even only use one or two of them in the template.
  ;; You can also insert or format any text in the template as shown in the following examples.
  ;; :quick-capture-templates
- ;; {:text "[[quick capture]] **{time}**: \n - {text} from {url}"
- ;;  :media "[[quick capture]] **{time}**: \n - {url}"}
+ ;; {:text "[[quick capture]] **{time}**: {text} from {url}"
+ ;;  :media "[[quick capture]] **{time}**: {url}"}
  }

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

@@ -20,16 +20,15 @@
     "@types/react-dom": "^17.0.0",
     "autoprefixer": "^10.4.7",
     "concurrently": "^7.2.1",
-    "esbuild": "^0.14.43",
+    "esbuild": "^0.14.48",
     "mobx": "^6.6.0",
     "mobx-react-lite": "^3.4.0",
     "perfect-freehand": "^1.1.0",
     "postcss": "^8.4.14",
     "react": "^17.0.0",
     "react-dom": "^17.0.0",
-    "react-select": "^5.3.2",
     "rimraf": "3.0.2",
-    "shadow-cljs": "^2.19.3",
+    "shadow-cljs": "^2.19.5",
     "tsup": "^6.1.2",
     "typescript": "^4.7.3",
     "zx": "^6.2.4",

+ 3 - 0
tldraw/apps/tldraw-logseq/src/app.tsx

@@ -13,6 +13,7 @@ import { AppUI } from '~components/AppUI'
 import { ContextBar } from '~components/ContextBar/ContextBar'
 import { useFileDrop } from '~hooks/useFileDrop'
 import { usePaste } from '~hooks/usePaste'
+import { useQuickAdd } from '~hooks/useQuickAdd'
 import { LogseqContext } from '~lib/logseq-context'
 import { Shape, shapes } from '~lib/shapes'
 import {
@@ -62,6 +63,7 @@ export const App = function App({
 }: LogseqTldrawProps): JSX.Element {
   const onFileDrop = useFileDrop()
   const onPaste = usePaste()
+  const onQuickAdd = useQuickAdd()
 
   const Page = React.useMemo(() => React.memo(PageComponent), [])
   return (
@@ -71,6 +73,7 @@ export const App = function App({
         Tools={tools}
         onFileDrop={onFileDrop}
         onPaste={onPaste}
+        onCanvasDBClick={onQuickAdd}
         {...props}
       >
         <div className="logseq-tldraw logseq-tldraw-wrapper">

+ 8 - 13
tldraw/apps/tldraw-logseq/src/components/inputs/ColorInput.tsx

@@ -7,21 +7,16 @@ interface ColorInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
 export function ColorInput({ label, value, onChange, ...rest }: ColorInputProps) {
   const ref = React.useRef<HTMLDivElement>(null)
   const [computedValue, setComputedValue] = React.useState(value)
-  let varName: string | undefined
+
   // TODO: listen to theme change?
-  if (value?.toString().startsWith('var') && ref.current) {
-    varName = /var\((.*)\)/.exec(value.toString())?.[1]
-    if (varName) {
-      const newValue = getComputedStyle(ref.current).getPropertyValue(varName).trim();
-      if (newValue !== computedValue) {
-        setComputedValue(newValue)
+  React.useEffect(() => {
+    if (value?.toString().startsWith('var') && ref.current) {
+      const varName = /var\((.*)\)/.exec(value.toString())?.[1]
+      if (varName) {
+        setComputedValue(getComputedStyle(ref.current).getPropertyValue(varName).trim())
       }
     }
-  }
-
-  if (varName) {
-    return null
-  }
+  }, [value])
 
   return (
     <div className="input" ref={ref}>
@@ -32,7 +27,7 @@ export function ColorInput({ label, value, onChange, ...rest }: ColorInputProps)
           name={`color-${label}`}
           type="color"
           value={computedValue}
-          onChange={e => {
+          onChange={(e) => {
             setComputedValue(e.target.value)
             onChange?.(e)
           }}

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

@@ -8,7 +8,6 @@ import {
   uniqueId,
 } from '@tldraw/core'
 import type { TLReactCallbacks } from '@tldraw/react'
-import { transaction } from 'mobx'
 import * as React from 'react'
 import { LogseqPortalShape, Shape } from '~lib'
 

+ 11 - 0
tldraw/apps/tldraw-logseq/src/hooks/useQuickAdd.ts

@@ -0,0 +1,11 @@
+import type { TLReactCallbacks } from '@tldraw/react'
+import React from 'react'
+import type { Shape } from '~lib'
+
+export function useQuickAdd() {
+  return React.useCallback<TLReactCallbacks<Shape>['onCanvasDBClick']>(async app => {
+    app.selectTool('logseq-portal', {
+      quick: true,
+    })
+  }, [])
+}

+ 15 - 13
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import { MagnifyingGlassIcon } from '@radix-ui/react-icons'
 import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
-import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
+import { HTMLContainer, TLComponentProps, TLContextBarProps, useApp } from '@tldraw/react'
 import { makeObservable } from 'mobx'
 import { observer } from 'mobx-react-lite'
 import * as React from 'react'
@@ -93,7 +93,6 @@ const LogseqPortalShapeHeader = observer(
 
 export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   static id = 'logseq-portal'
-  static smart = true
 
   static defaultProps: LogseqPortalShapeProps = {
     id: 'logseq-portal',
@@ -114,7 +113,6 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   hideRotateHandle = true
   canChangeAspectRatio = true
   canFlip = true
-  canActivate = true
   canEdit = true
 
   constructor(props = {} as Partial<LogseqPortalShapeProps>) {
@@ -134,6 +132,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   }
 
   ReactContextBar = observer(() => {
+    const app = useApp<Shape>()
     return (
       <>
         <ColorInput
@@ -143,6 +142,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
             this.update({
               fill: e.target.value,
             })
+            app.persist(true)
           }}
         />
         <ColorInput
@@ -152,6 +152,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
             this.update({
               stroke: e.target.value,
             })
+            app.persist(true)
           }}
         />
         <SwitchInput
@@ -165,6 +166,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
               size: [this.props.size[0], collapsing ? HEADER_HEIGHT : this.props.collapsedHeight],
               collapsedHeight: collapsing ? originalHeight : this.props.collapsedHeight,
             })
+            app.persist()
           }}
         />
       </>
@@ -180,8 +182,9 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     const isMoving = useCameraMovingRef()
     const { Page } = React.useContext(LogseqContext)
     const isSelected = app.selectedIds.has(this.id)
+    const isCreating = app.isIn('logseq-portal.creating') && !pageId
     const tlEventsEnabled =
-      isMoving || (isSelected && !isEditing) || app.selectedTool.id !== 'select'
+      (isMoving || (isSelected && !isEditing) || app.selectedTool.id !== 'select') && !isCreating
     const stop = React.useCallback(
       e => {
         if (!tlEventsEnabled) {
@@ -211,15 +214,14 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     }, [isEditing, this.props.collapsed])
 
     const onPageNameChanged = React.useCallback((id: string) => {
-      app.wrapUpdate(() => {
-        this.update({
-          pageId: id,
-          size: [600, 320],
-          blockType: 'P',
-        })
-        this.setDraft(false)
-        app.clearEditingShape()
+      this.update({
+        pageId: id,
+        size: [600, 320],
+        blockType: 'P',
       })
+      app.selectTool('select')
+      app.history.resume()
+      app.history.persist()
     }, [])
 
     if (!Page) {
@@ -244,7 +246,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
             pointerEvents: isEditing ? 'all' : 'none',
           }}
         >
-          {this.draft ? (
+          {isCreating ? (
             <LogseqQuickSearch onChange={onPageNameChanged} />
           ) : (
             <div

+ 0 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx

@@ -35,8 +35,6 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
 
   canEdit = true
 
-  canActivate = true
-
   ReactContextBar = observer(() => {
     const { embedId } = this.props
     const rInput = React.useRef<HTMLInputElement>(null)

+ 1 - 0
tldraw/apps/tldraw-logseq/src/lib/shapes/text/TextLabel.tsx

@@ -76,6 +76,7 @@ export const TextLabel = React.memo(function TextLabel({
 
   const handleBlur = React.useCallback(
     (e: React.FocusEvent<HTMLTextAreaElement>) => {
+      if (!isEditing) return
       e.currentTarget.setSelectionRange(0, 0)
       onBlur?.()
     },

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

@@ -1,11 +0,0 @@
-import { TLBoxTool, TLDotTool } from '@tldraw/core'
-import type { TLReactEventMap } from '@tldraw/react'
-import { Shape, LogseqPortalShape } from '~lib/shapes'
-
-export class LogseqPortalTool extends TLDotTool<LogseqPortalShape, Shape, TLReactEventMap> {
-  static id = 'logseq-portal'
-  static shortcut = ['i']
-  Shape = LogseqPortalShape
-}
-
-export {}

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

@@ -0,0 +1,17 @@
+import { TLApp, TLTool } from '@tldraw/core'
+import type { TLReactEventMap } from '@tldraw/react'
+import { LogseqPortalShape, Shape } from '~lib/shapes'
+import { CreatingState, IdleState } from './states'
+
+export class LogseqPortalTool extends TLTool<
+  Shape,
+  TLReactEventMap,
+  TLApp<Shape, TLReactEventMap>
+> {
+  static id = 'logseq-portal'
+  static shortcut = ['i']
+  static states = [IdleState, CreatingState]
+  static initial = 'idle'
+
+  Shape = LogseqPortalShape
+}

+ 1 - 0
tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/index.ts

@@ -0,0 +1 @@
+export * from './LogseqPortalTool'

+ 68 - 0
tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/CreatingState.tsx

@@ -0,0 +1,68 @@
+import { TLApp, TLTargetType, TLToolState, uniqueId } from '@tldraw/core'
+import type { TLReactEventMap, TLReactEvents } from '@tldraw/react'
+import Vec from '@tldraw/vec'
+import { transaction } from 'mobx'
+import { LogseqPortalShape, Shape } from '~lib/shapes'
+import type { LogseqPortalTool } from '../LogseqPortalTool'
+
+export class CreatingState extends TLToolState<
+  Shape,
+  TLReactEventMap,
+  TLApp<Shape, TLReactEventMap>,
+  LogseqPortalTool
+> {
+  static id = 'creating'
+
+  creatingShape?: LogseqPortalShape
+
+  offset: number[] = [0, 0]
+
+  onEnter = () => {
+    this.app.history.pause()
+    transaction(() => {
+      const shape = new LogseqPortalShape({
+        id: uniqueId(),
+        parentId: this.app.currentPage.id,
+        point: Vec.sub(this.app.inputs.originPoint, this.offset),
+        size: LogseqPortalShape.defaultProps.size,
+      } as any)
+      this.creatingShape = shape
+      this.app.currentPage.addShapes(shape)
+      this.app.setEditingShape(shape)
+    })
+  }
+
+  onPointerDown: TLReactEvents<Shape>['pointer'] = info => {
+    switch (info.type) {
+      case TLTargetType.Shape: {
+        if (info.shape === this.creatingShape) return
+        this.app.selectTool('select')
+        break
+      }
+      case TLTargetType.Selection: {
+        break
+      }
+      case TLTargetType.Handle: {
+        break
+      }
+      case TLTargetType.Canvas: {
+        if (!info.order) {
+          this.app.selectTool('select')
+        }
+        break
+      }
+    }
+  }
+
+  onExit = () => {
+    if (!this.creatingShape) return
+    this.app.clearEditingShape()
+    this.app.history.resume()
+
+    if (this.creatingShape?.props.pageId) {
+      this.app.setSelectedShapes([this.creatingShape.id])
+    } else {
+      this.app.deleteShapes([this.creatingShape.id])
+    }
+  }
+}

+ 24 - 0
tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/IdleState.tsx

@@ -0,0 +1,24 @@
+import { TLApp, TLCursor, TLToolState } from '@tldraw/core'
+import type { TLReactEventMap, TLReactEvents } from '@tldraw/react'
+import type { Shape } from '~lib/shapes'
+import type { LogseqPortalTool } from '../LogseqPortalTool'
+
+export class IdleState extends TLToolState<
+  Shape,
+  TLReactEventMap,
+  TLApp<Shape, TLReactEventMap>,
+  LogseqPortalTool
+> {
+  static id = 'idle'
+  cursor = TLCursor.Cross
+
+  onEnter = ({ quick }: { quick?: boolean }) => {
+    if (quick) {
+      this.tool.transition('creating')
+    }
+  }
+
+  onPointerDown: TLReactEvents<Shape>['pointer'] = e => {
+    this.tool.transition('creating')
+  }
+}

+ 2 - 0
tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/index.ts

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

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/index.ts

@@ -8,4 +8,4 @@ export * from './PencilTool'
 export * from './PolygonTool'
 export * from './TextTool'
 export * from './YouTubeTool'
-export * from './LogseqPortalTool'
+export * from './LogseqPortalTool'

+ 1 - 1
tldraw/apps/tldraw-logseq/tsup.config.ts

@@ -3,7 +3,7 @@ import { defineConfig } from 'tsup'
 export default defineConfig({
   target: 'es6',
   platform: 'browser',
-  format: ['cjs', 'esm'],
+  format: ['cjs'],
   entry: ['src/index.ts'],
   clean: false,
   loader: {

+ 0 - 1
tldraw/package.json

@@ -44,7 +44,6 @@
     "@types/node": "^17.0.42",
     "@types/react": "^17.0.0",
     "@types/react-dom": "^17.0.0",
-    "@types/vscode": "^1.68.0",
     "@typescript-eslint/eslint-plugin": "^5.27.1",
     "@typescript-eslint/parser": "^5.27.1",
     "eslint": "^8.17.0",

+ 9 - 31
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -2,37 +2,24 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import { Vec } from '@tldraw/vec'
-import { action, computed, makeObservable, observable, toJS, transaction } from 'mobx'
-import { BoundsUtils, KeyUtils } from '~utils'
+import { action, computed, makeObservable, observable, transaction } from 'mobx'
+import { GRID_SIZE } from '~constants'
 import {
-  TLSelectTool,
   TLInputs,
-  TLPage,
-  TLViewport,
-  TLShape,
-  TLPageModel,
-  TLToolConstructor,
-  TLShapeConstructor,
-  TLShapeModel,
+  TLPage, TLPageModel, TLSelectTool, TLShape, TLShapeConstructor,
+  TLShapeModel, TLToolConstructor, TLViewport
 } from '~lib'
+import { TLApi } from '~lib/TLApi'
+import { TLCursors } from '~lib/TLCursors'
 import type {
-  TLBounds,
-  TLEvents,
-  TLSubscription,
+  TLAsset, TLBounds, TLCallback, TLEventMap, TLEvents, TLShortcut, TLStateEvents, TLSubscription,
   TLSubscriptionEventInfo,
-  TLSubscriptionEventName,
-  TLCallback,
-  TLShortcut,
-  TLEventMap,
-  TLStateEvents,
-  TLAsset,
+  TLSubscriptionEventName
 } from '~types'
+import { BoundsUtils, KeyUtils } from '~utils'
 import { TLHistory } from '../TLHistory'
 import { TLSettings } from '../TLSettings'
 import { TLRootState } from '../TLState'
-import { TLApi } from '~lib/TLApi'
-import { TLCursors } from '~lib/TLCursors'
-import { GRID_SIZE } from '~constants'
 
 export interface TLDocumentModel<S extends TLShape = TLShape, A extends TLAsset = TLAsset> {
   currentPageId: string
@@ -673,15 +660,6 @@ export class TLApp<
 
   Shapes = new Map<string, TLShapeConstructor<S>>()
 
-  get SmartShape() {
-    for (const S of this.Shapes.values()) {
-      if (S.smart) {
-        return S
-      }
-    }
-    return null
-  }
-
   registerShapes = (Shapes: TLShapeConstructor<S>[]) => {
     Shapes.forEach(Shape => this.Shapes.set(Shape.id, Shape))
   }

+ 2 - 2
tldraw/packages/core/src/lib/TLHistory.ts

@@ -1,5 +1,5 @@
-import { computed, makeObservable, observable } from 'mobx'
-import { TLApp, TLPage, TLDocumentModel, TLShape } from '~lib'
+import { computed, makeObservable } from 'mobx'
+import { TLApp, TLDocumentModel, TLPage, TLShape } from '~lib'
 import type { TLEventMap } from '~types'
 import { deepEqual } from '~utils'
 

+ 2 - 6
tldraw/packages/core/src/lib/TLPage/TLPage.ts

@@ -43,6 +43,7 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
         editingShape: toJS(this.app.editingShape),
       }),
       (curr, prev) => {
+        if (this.app.isInAny('creating')) return
         this.cleanup(curr, prev)
       }
     )
@@ -260,16 +261,11 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
       }
     })
 
-    // Cleanup inactive drafts
-    const shapesToDelete = this.shapes.filter(s => s.draft && this.app.editingShape !== s)
-
-    if (!deepEqual(updated, curr) || shapesToDelete.length) {
+    if (!deepEqual(updated, curr)) {
       this.update({
         bindings: updated.bindings,
       })
 
-      this.removeShapes(...shapesToDelete)
-
       updated.shapes.forEach(shape => {
         this.getShapeById(shape.id)?.update(shape)
       })

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

@@ -4,7 +4,7 @@ import { TLShape, TLResizeInfo, TLShapeProps } from '../TLShape'
 import { BoundsUtils } from '~utils'
 
 export interface TLDotShapeProps extends TLShapeProps {
-  radius: number
+  radius?: number
 }
 
 export class TLDotShape<P extends TLDotShapeProps = TLDotShapeProps, M = any> extends TLShape<
@@ -35,7 +35,7 @@ export class TLDotShape<P extends TLDotShapeProps = TLDotShapeProps, M = any> ex
     const {
       props: {
         point: [x, y],
-        radius,
+        radius = 0,
       },
     } = this
     return {
@@ -56,7 +56,7 @@ export class TLDotShape<P extends TLDotShapeProps = TLDotShapeProps, M = any> ex
 
   onResize = (initialProps: any, info: TLResizeInfo): this => {
     const {
-      props: { radius },
+      props: { radius = 0 },
     } = this
     return this.update({
       point: [

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

@@ -6,7 +6,7 @@ import {
   intersectRayBounds,
 } from '@tldraw/intersect'
 import Vec from '@tldraw/vec'
-import { action, computed, makeObservable, observable, toJS } from 'mobx'
+import { action, computed, makeObservable, observable, toJS, transaction } from 'mobx'
 import { BINDING_DISTANCE } from '~constants'
 import type { TLAsset, TLBounds, TLHandle, TLResizeCorner, TLResizeEdge } from '~types'
 import { BoundsUtils, deepCopy, PointUtils } from '~utils'
@@ -18,7 +18,6 @@ export type TLShapeModel<P extends TLShapeProps = TLShapeProps> = {
 export interface TLShapeConstructor<S extends TLShape = TLShape> {
   new (props: any): S
   id: string
-  smart: boolean
 }
 
 export type TLFlag = boolean
@@ -81,8 +80,6 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
     makeObservable(this)
   }
 
-  // there should be only one Shape that is smart (created by double click canvas)
-  static smart: boolean
   static type: string
 
   @observable props: P
@@ -103,14 +100,11 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
   canFlip: TLFlag = true
   canEdit: TLFlag = false
   canBind: TLFlag = false
-  canActivate: TLFlag = false
-  
+
   @observable nonce = 0
 
   bindingDistance = BINDING_DISTANCE
 
-  // For smart shape
-  @observable private _draft = false
   @observable private isDirty = false
   @observable private lastSerialized: TLShapeModel<P> | undefined
 
@@ -120,13 +114,8 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
     return this.props.id
   }
 
-  @computed
-  get draft() {
-    return this._draft
-  }
-
-  @action setDraft(draft: boolean) {
-    this._draft = draft
+  @action setNonce(nonce: number) {
+    this.nonce = nonce
   }
 
   @action setIsDirty(isDirty: boolean) {
@@ -283,9 +272,11 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
 
   protected getCachedSerialized = (): TLShapeModel<P> => {
     if (this.isDirty || !this.lastSerialized) {
-      this.nonce = Date.now()
-      this.setIsDirty(false)
-      this.setLastSerialized(this.getSerialized())
+      transaction(() => {
+        this.setNonce(Date.now())
+        this.setIsDirty(false)
+        this.setLastSerialized(this.getSerialized())
+      })
     }
     if (this.lastSerialized) {
       return this.lastSerialized
@@ -295,7 +286,7 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
 
   @computed
   get serialized(): TLShapeModel<P> | null {
-    return this.draft ? null : this.getCachedSerialized()
+    return this.getCachedSerialized()
   }
 
   validateProps = (

+ 2 - 3
tldraw/packages/core/src/lib/tools/TLDotTool/TLDotTool.ts

@@ -1,9 +1,9 @@
-import { TLApp, TLBoxShape, TLShape, TLTool } from '~lib'
+import { TLApp, TLDotShape, TLShape, TLTool } from '~lib'
 import { TLCursor, TLEventMap } from '~types'
 import { CreatingState, IdleState } from './states'
 
 export abstract class TLDotTool<
-  T extends TLBoxShape = TLBoxShape,
+  T extends TLDotShape = TLDotShape,
   S extends TLShape = TLShape,
   K extends TLEventMap = TLEventMap,
   R extends TLApp<S, K> = TLApp<S, K>
@@ -19,7 +19,6 @@ export abstract class TLDotTool<
   abstract Shape: {
     new (props: Partial<T['props']>): T
     id: string
-    smart: boolean
     defaultProps: T['props']
   }
 }

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

@@ -1,12 +1,12 @@
 import Vec from '@tldraw/vec'
-import { TLApp, TLShape, TLToolState, TLBoxShape } from '~lib'
+import { TLApp, TLShape, TLToolState, TLDotShape } from '~lib'
 import { uniqueId } from '~utils'
 import type { TLEventMap, TLStateEvents } from '~types'
 import type { TLDotTool } from '../TLDotTool'
 import { transaction } from 'mobx'
 
 export class CreatingState<
-  S extends TLBoxShape,
+  S extends TLDotShape,
   T extends S & TLShape,
   K extends TLEventMap,
   R extends TLApp<S, K>,
@@ -26,9 +26,6 @@ export class CreatingState<
       point: Vec.sub(this.app.inputs.originPoint, this.offset),
       size: Shape.defaultProps.size,
     } as any)
-    if (Shape.smart) {
-      shape.setDraft(true)
-    }
     this.creatingShape = shape
   }
 
@@ -46,11 +43,7 @@ export class CreatingState<
       const shape = this.creatingShape
       transaction(() => {
         this.app.currentPage.addShapes(shape)
-        if (this.tool.Shape.smart && shape.draft) {
-          this.app.setEditingShape(shape)
-        } else {
-          this.app.setSelectedShapes([shape])
-        }
+        this.app.setSelectedShapes([shape])
       })
     }
     if (!this.app.settings.isToolLocked) {

+ 2 - 2
tldraw/packages/core/src/lib/tools/TLDotTool/states/IdleState.tsx

@@ -1,10 +1,10 @@
-import { TLBoxShape, TLApp, TLShape, TLToolState } from '~lib'
+import { TLDotShape, TLApp, TLShape, TLToolState } from '~lib'
 import type { TLEventMap, TLStateEvents } from '~types'
 import type { TLDotTool } from '../TLDotTool'
 
 export class IdleState<
   S extends TLShape,
-  T extends S & TLBoxShape,
+  T extends S & TLDotShape,
   K extends TLEventMap,
   R extends TLApp<S, K>,
   P extends TLDotTool<T, S, K, R>

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

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

+ 2 - 15
tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts

@@ -1,4 +1,4 @@
-import { TLShape, TLApp, TLSelectTool, TLToolState } from '~lib'
+import { TLShape, TLApp, TLSelectTool, TLToolState, TLBoxShape } from '~lib'
 import { TLEventMap, TLEvents, TLShortcut, TLTargetType } from '~types'
 import { PointUtils } from '~utils'
 
@@ -47,7 +47,6 @@ export class IdleState<
   onPointerDown: TLEvents<S>['pointer'] = (info, event) => {
     const {
       selectedShapes,
-      selectedShapesArray,
       inputs: { ctrlKey },
     } = this.app
 
@@ -57,15 +56,6 @@ export class IdleState<
       return
     }
 
-    if (selectedShapesArray.length === 1 && selectedShapesArray[0].canEdit) {
-      this.tool.transition('editingShape', {
-        type: TLTargetType.Shape,
-        shape: selectedShapesArray[0],
-        order: 0,
-      })
-      return
-    }
-
     switch (info.type) {
       case TLTargetType.Selection: {
         switch (info.handle) {
@@ -93,11 +83,8 @@ export class IdleState<
           const { selectionBounds, inputs } = this.app
           if (selectionBounds && PointUtils.pointInBounds(inputs.currentPoint, selectionBounds)) {
             this.tool.transition('pointingShapeBehindBounds', info)
-          } else if (!info.shape.draft) {
-            this.tool.transition('pointingShape', info)
           } else {
-            // as if clicking the canvas
-            this.tool.transition('pointingCanvas')
+            this.tool.transition('pointingShape', info)
           }
         }
         break

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

@@ -10,21 +10,8 @@ export class PointingBoundsBackgroundState<
 > extends TLToolState<S, K, R, P> {
   static id = 'pointingBoundsBackground'
 
-  private pointedSelectedShape?: S
-
   cursor = TLCursor.Move
 
-  onEnter = () => {
-    // If there is exactly a single shape
-    if (this.app.selectedShapes.size === 1) {
-      this.pointedSelectedShape = this.app.selectedShapesArray[0]
-    }
-  }
-
-  onExit = () => {
-    this.pointedSelectedShape = undefined
-  }
-
   onWheel: TLEvents<S>['wheel'] = (info, e) => {
     this.onPointerMove(info, e)
   }
@@ -37,9 +24,6 @@ export class PointingBoundsBackgroundState<
   }
 
   onPointerUp: TLEvents<S>['pointer'] = () => {
-    if (this.pointedSelectedShape?.canActivate) {
-      this.app.setEditingShape(this.pointedSelectedShape.id)
-    }
     this.app.setSelectedShapes([])
     this.tool.transition('idle')
   }

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

@@ -1,8 +1,6 @@
 import { Vec } from '@tldraw/vec'
-import { transaction } from 'mobx'
 import { TLApp, TLSelectTool, TLShape, TLToolState } from '~lib'
 import type { TLEventMap, TLEvents } from '~types'
-import { uniqueId } from '~utils'
 
 export class PointingCanvasState<
   S extends TLShape,
@@ -42,20 +40,6 @@ export class PointingCanvasState<
   }
 
   onDoubleClick: TLEvents<S>['pointer'] = () => {
-    transaction(() => {
-      const Shape = this.app.SmartShape
-      if (Shape) {
-        const shape = new Shape({
-          id: uniqueId(),
-          type: Shape.id,
-          parentId: this.app.currentPage.id,
-          point: [...this.app.inputs.originPoint],
-        })
-        shape.setDraft(true)
-        this.app.history.pause()
-        this.app.setEditingShape(shape)
-        this.app.currentPage.addShapes(shape)
-      }
-    })
+    this.app.notify('canvas-dbclick', { point: this.app.inputs.originPoint })
   }
 }

+ 14 - 2
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingSelectedShapeState.ts

@@ -1,6 +1,6 @@
 import { Vec } from '@tldraw/vec'
-import { TLApp, TLSelectTool, TLToolState, TLShape } from '~lib'
-import type { TLEventMap, TLEvents, TLEventShapeInfo } from '~types'
+import { TLApp, TLSelectTool, TLToolState, TLShape, TLBoxShape } from '~lib'
+import { TLEventMap, TLEvents, TLEventShapeInfo, TLTargetType } from '~types'
 
 export class PointingSelectedShapeState<
   S extends TLShape,
@@ -33,12 +33,24 @@ export class PointingSelectedShapeState<
 
   onPointerUp: TLEvents<S>['pointer'] = () => {
     const { shiftKey } = this.app.inputs
+    const { selectedShapesArray } = this.app
     if (!this.pointedSelectedShape) throw Error('Expected a pointed selected shape')
     if (shiftKey) {
       const { selectedIds } = this.app
       const next = Array.from(selectedIds.values())
       next.splice(next.indexOf(this.pointedSelectedShape.id), 1)
       this.app.setSelectedShapes(next)
+    } else if (
+      selectedShapesArray.length === 1 &&
+      this.pointedSelectedShape.canEdit &&
+      this.pointedSelectedShape instanceof TLBoxShape
+    ) {
+      this.tool.transition('editingShape', {
+        shape: this.pointedSelectedShape,
+        order: 0,
+        type: TLTargetType.Shape,
+      })
+      return
     } else {
       this.app.setSelectedShapes([this.pointedSelectedShape.id])
     }

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

@@ -67,7 +67,6 @@ export class ResizingState<
     // @ts-expect-error maybe later
     this.snapshots = Object.fromEntries(
       selectedShapesArray
-        .filter(s => !s.draft)
         .map(shape => {
           const bounds = { ...shape.bounds }
           const [cx, cy] = BoundsUtils.getBoundsCenter(bounds)

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

@@ -168,6 +168,10 @@ export type TLSubscriptionEvent =
       event: 'delete-assets'
       info: { assets: TLAsset[] }
     }
+    | {
+      event: 'canvas-dbclick'
+      info: { point: number[] }
+    }
 
 export type TLSubscriptionEventName = TLSubscriptionEvent['event']
 

+ 1 - 1
tldraw/packages/react/src/components/Canvas/Canvas.tsx

@@ -151,7 +151,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
               isSelected={true}
             />
           ))}
-          {hoveredShape && !hoveredShape.draft && (
+          {hoveredShape && app.isInAny('creating') && (
             <Indicator key={'hovered_indicator_' + hoveredShape.id} shape={hoveredShape} />
           )}
           {brush && components.Brush && <components.Brush bounds={brush} />}

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

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

+ 0 - 1
tldraw/packages/react/src/lib/TLReactShape.tsx

@@ -29,7 +29,6 @@ export interface TLComponentProps<M = unknown> extends TLCommonShapeProps<M> {
 export interface TLReactShapeConstructor<S extends TLReactShape = TLReactShape> {
   new (props: S['props'] & { type: any }): S
   id: string
-  smart: boolean
 }
 
 export abstract class TLReactShape<P extends TLShapeProps = TLShapeProps, M = any> extends TLShape<

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

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

+ 140 - 196
tldraw/yarn.lock

@@ -109,7 +109,7 @@
   dependencies:
     "@babel/types" "^7.17.0"
 
-"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7":
+"@babel/helper-module-imports@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
   integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==
@@ -254,7 +254,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.17.12":
+"@babel/plugin-syntax-jsx@^7.17.12":
   version "7.17.12"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.17.12.tgz#834035b45061983a491f60096f61a2e7c5674a47"
   integrity sha512-spyY3E3AURfxh/RHtjx5j6hs8am5NbUBGfcZ2vB3uShSpZdQyXSf5rR5Mk76vbtlAZOelyVQ71Fg0x9SG4fsog==
@@ -349,7 +349,7 @@
     "@babel/plugin-syntax-jsx" "^7.17.12"
     "@babel/types" "^7.17.12"
 
-"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.9.2":
   version "7.18.3"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
   integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
@@ -394,89 +394,6 @@
   resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
   integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
 
-"@emotion/babel-plugin@^11.7.1":
-  version "11.9.2"
-  resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95"
-  integrity sha512-Pr/7HGH6H6yKgnVFNEj2MVlreu3ADqftqjqwUvDy/OJzKFgxKeTQ+eeUf20FOTuHVkDON2iNa25rAXVYtWJCjw==
-  dependencies:
-    "@babel/helper-module-imports" "^7.12.13"
-    "@babel/plugin-syntax-jsx" "^7.12.13"
-    "@babel/runtime" "^7.13.10"
-    "@emotion/hash" "^0.8.0"
-    "@emotion/memoize" "^0.7.5"
-    "@emotion/serialize" "^1.0.2"
-    babel-plugin-macros "^2.6.1"
-    convert-source-map "^1.5.0"
-    escape-string-regexp "^4.0.0"
-    find-root "^1.1.0"
-    source-map "^0.5.7"
-    stylis "4.0.13"
-
-"@emotion/cache@^11.4.0", "@emotion/cache@^11.9.3":
-  version "11.9.3"
-  resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.9.3.tgz#96638449f6929fd18062cfe04d79b29b44c0d6cb"
-  integrity sha512-0dgkI/JKlCXa+lEXviaMtGBL0ynpx4osh7rjOXE71q9bIF8G+XhJgvi+wDu0B0IdCVx37BffiwXlN9I3UuzFvg==
-  dependencies:
-    "@emotion/memoize" "^0.7.4"
-    "@emotion/sheet" "^1.1.1"
-    "@emotion/utils" "^1.0.0"
-    "@emotion/weak-memoize" "^0.2.5"
-    stylis "4.0.13"
-
-"@emotion/hash@^0.8.0":
-  version "0.8.0"
-  resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
-  integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
-
-"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5":
-  version "0.7.5"
-  resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50"
-  integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
-
-"@emotion/react@^11.8.1":
-  version "11.9.3"
-  resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.9.3.tgz#f4f4f34444f6654a2e550f5dab4f2d360c101df9"
-  integrity sha512-g9Q1GcTOlzOEjqwuLF/Zd9LC+4FljjPjDfxSM7KmEakm+hsHXk+bYZ2q+/hTJzr0OUNkujo72pXLQvXj6H+GJQ==
-  dependencies:
-    "@babel/runtime" "^7.13.10"
-    "@emotion/babel-plugin" "^11.7.1"
-    "@emotion/cache" "^11.9.3"
-    "@emotion/serialize" "^1.0.4"
-    "@emotion/utils" "^1.1.0"
-    "@emotion/weak-memoize" "^0.2.5"
-    hoist-non-react-statics "^3.3.1"
-
-"@emotion/serialize@^1.0.2", "@emotion/serialize@^1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.4.tgz#ff31fd11bb07999611199c2229e152faadc21a3c"
-  integrity sha512-1JHamSpH8PIfFwAMryO2bNka+y8+KA5yga5Ocf2d7ZEiJjb7xlLW7aknBGZqJLajuLOvJ+72vN+IBSwPlXD1Pg==
-  dependencies:
-    "@emotion/hash" "^0.8.0"
-    "@emotion/memoize" "^0.7.4"
-    "@emotion/unitless" "^0.7.5"
-    "@emotion/utils" "^1.0.0"
-    csstype "^3.0.2"
-
-"@emotion/sheet@^1.1.1":
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.1.tgz#015756e2a9a3c7c5f11d8ec22966a8dbfbfac787"
-  integrity sha512-J3YPccVRMiTZxYAY0IOq3kd+hUP8idY8Kz6B/Cyo+JuXq52Ek+zbPbSQUrVQp95aJ+lsAW7DPL1P2Z+U1jGkKA==
-
-"@emotion/unitless@^0.7.5":
-  version "0.7.5"
-  resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
-  integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
-
-"@emotion/utils@^1.0.0", "@emotion/utils@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf"
-  integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==
-
-"@emotion/weak-memoize@^0.2.5":
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
-  integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
-
 "@eslint/eslintrc@^1.3.0":
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f"
@@ -2332,13 +2249,6 @@
   dependencies:
     "@types/react" "*"
 
-"@types/react-transition-group@^4.4.0":
-  version "4.4.4"
-  resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
-  integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
-  dependencies:
-    "@types/react" "*"
-
 "@types/react@*":
   version "18.0.12"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.12.tgz#cdaa209d0a542b3fcf69cf31a03976ec4cdd8840"
@@ -2379,11 +2289,6 @@
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
   integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
 
-"@types/vscode@^1.68.0":
-  version "1.68.0"
-  resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.68.0.tgz#c0864e4ea43c509bfc6b53f4e91aa923fd0475b8"
-  integrity sha512-duBwEK5ta/eBBMJMQ7ECMEsMvlE3XJdRGh3xoS1uOO4jl2Z4LPBl5vx8WvBP10ERAgDRmIt/FaSD4RHyBGbChw==
-
 "@types/which@^2.0.1":
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.1.tgz#27ecd67f915b7c3d6ba552135bb1eecd66e63501"
@@ -2804,15 +2709,6 @@ babel-plugin-jest-hoist@^28.1.1:
     "@types/babel__core" "^7.1.14"
     "@types/babel__traverse" "^7.0.6"
 
-babel-plugin-macros@^2.6.1:
-  version "2.8.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
-  integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
-  dependencies:
-    "@babel/runtime" "^7.7.2"
-    cosmiconfig "^6.0.0"
-    resolve "^1.12.0"
-
 babel-preset-current-node-syntax@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b"
@@ -3528,7 +3424,7 @@ conventional-recommended-bump@^6.1.0:
     meow "^8.0.0"
     q "^1.5.1"
 
-convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
+convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
   integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
@@ -3545,17 +3441,6 @@ core-util-is@~1.0.0:
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
   integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
 
-cosmiconfig@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
-  integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==
-  dependencies:
-    "@types/parse-json" "^4.0.0"
-    import-fresh "^3.1.0"
-    parse-json "^5.0.0"
-    path-type "^4.0.0"
-    yaml "^1.7.2"
-
 cosmiconfig@^7.0.0:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d"
@@ -3845,14 +3730,6 @@ dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9:
   resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56"
   integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==
 
-dom-helpers@^5.0.1:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
-  integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
-  dependencies:
-    "@babel/runtime" "^7.8.7"
-    csstype "^3.0.2"
-
 domain-browser@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -3963,102 +3840,202 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.43.tgz#59bf3edad6863c27aa92bbb5c1d83a9a5c981495"
   integrity sha512-kqFXAS72K6cNrB6RiM7YJ5lNvmWRDSlpi7ZuRZ1hu1S3w0zlwcoCxWAyM23LQUyZSs1PbjHgdbbfYAN8IGh6xg==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.48.tgz#7e6394a0e517f738641385aaf553c7e4fb6d1ae3"
+  integrity sha512-3aMjboap/kqwCUpGWIjsk20TtxVoKck8/4Tu19rubh7t5Ra0Yrpg30Mt1QXXlipOazrEceGeWurXKeFJgkPOUg==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.43.tgz#0258704edf92ce2463af6d2900b844b5423bed63"
   integrity sha512-bKS2BBFh+7XZY9rpjiHGRNA7LvWYbZWP87pLehggTG7tTaCDvj8qQGOU/OZSjCSKDYbgY7Q+oDw8RlYQ2Jt2BA==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.48.tgz#6877566be0f82dd5a43030c0007d06ece7f7c02f"
+  integrity sha512-vptI3K0wGALiDq+EvRuZotZrJqkYkN5282iAfcffjI5lmGG9G1ta/CIVauhY42MBXwEgDJkweiDcDMRLzBZC4g==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.43.tgz#72a47295678d4aa0656979baa8cf6d5c8c92656f"
   integrity sha512-/3PSilx011ttoieRGkSZ0XV8zjBf2C9enV4ScMMbCT4dpx0mFhMOpFnCHkOK0pWGB8LklykFyHrWk2z6DENVUg==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.48.tgz#ea3caddb707d88f844b1aa1dea5ff3b0a71ef1fd"
+  integrity sha512-gGQZa4+hab2Va/Zww94YbshLuWteyKGD3+EsVon8EWTWhnHFRm5N9NbALNbwi/7hQ/hM1Zm4FuHg+k6BLsl5UA==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.43.tgz#5f5823170b8d85b888957f0794e186caac447aca"
   integrity sha512-1HyFUKs8DMCBOvw1Qxpr5Vv/ThNcVIFb5xgXWK3pyT40WPvgYIiRTwJCvNs4l8i5qWF8/CK5bQxJVDjQvtv0Yw==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.48.tgz#4e5eaab54df66cc319b76a2ac0e8af4e6f0d9c2f"
+  integrity sha512-bFjnNEXjhZT+IZ8RvRGNJthLWNHV5JkCtuOFOnjvo5pC0sk2/QVk0Qc06g2PV3J0TcU6kaPC3RN9yy9w2PSLEA==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.43.tgz#e4a48b08181053837e6cd9bda19ae0af94d493b0"
   integrity sha512-FNWc05TPHYgaXjbPZO5/rJKSBslfG6BeMSs8GhwnqAKP56eEhvmzwnIz1QcC9cRVyO+IKqWNfmHFkCa1WJTULA==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.48.tgz#47b5abc7426eae66861490ffbb380acc67af5b15"
+  integrity sha512-1NOlwRxmOsnPcWOGTB10JKAkYSb2nue0oM1AfHWunW/mv3wERfJmnYlGzL3UAOIUXZqW8GeA2mv+QGwq7DToqA==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.43.tgz#386e780d36c1dedf3a1cdab79e0bbacd873274e6"
   integrity sha512-amrYopclz3VohqisOPR6hA3GOWA3LZC1WDLnp21RhNmoERmJ/vLnOpnrG2P/Zao+/erKTCUqmrCIPVtj58DRoA==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.48.tgz#e8c54c8637cd44feed967ea12338b0a4da3a7b11"
+  integrity sha512-gXqKdO8wabVcYtluAbikDH2jhXp+Klq5oCD5qbVyUG6tFiGhrC9oczKq3vIrrtwcxDQqK6+HDYK8Zrd4bCA9Gw==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.43.tgz#040ed6b9ebf06d73acdf2acce7f1cd0c12fbc6a5"
   integrity sha512-KoxoEra+9O3AKVvgDFvDkiuddCds6q71owSQEYwjtqRV7RwbPzKxJa6+uyzUulHcyGVq0g15K0oKG5CFBcvYDw==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.48.tgz#229cf3246de2b7937c3ac13fac622d4d7a1344c5"
+  integrity sha512-ghGyDfS289z/LReZQUuuKq9KlTiTspxL8SITBFQFAFRA/IkIvDpnZnCAKTCjGXAmUqroMQfKJXMxyjJA69c/nQ==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.43.tgz#8abbb7594ab6a008f2aae72d95d8a4fdc59d9000"
   integrity sha512-EwINwGMyiJMgBby5/SbMqKcUhS5AYAZ2CpEBzSowsJPNBJEdhkCTtEjk757TN/wxgbu3QklqDM6KghY660QCUw==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.48.tgz#7c0e7226c02c42aacc5656c36977493dc1e96c4f"
+  integrity sha512-vni3p/gppLMVZLghI7oMqbOZdGmLbbKR23XFARKnszCIBpEMEDxOMNIKPmMItQrmH/iJrL1z8Jt2nynY0bE1ug==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.43.tgz#4e8e9ce77cbf7efec65e79e512b3d2fbd2da398f"
   integrity sha512-UlSpjMWllAc70zYbHxWuDS3FJytyuR/gHJYBr8BICcTNb/TSOYVBg6U7b3jZ3mILTrgzwJUHwhEwK18FZDouUQ==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.48.tgz#0af1eda474b5c6cc0cace8235b74d0cb8fcf57a7"
+  integrity sha512-3CFsOlpoxlKPRevEHq8aAntgYGYkE1N9yRYAcPyng/p4Wyx0tPR5SBYsxLKcgPB9mR8chHEhtWYz6EZ+H199Zw==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.43.tgz#9e41ee5e099c0ffdfd150da154330c2c0226cc96"
   integrity sha512-e6YzQUoDxxtyamuF12eVzzRC7bbEFSZohJ6igQB9tBqnNmIQY3fI6Cns3z2wxtbZ3f2o6idkD2fQnlvs2902Dg==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.48.tgz#de4d1fa6b77cdcd00e2bb43dd0801e4680f0ab52"
+  integrity sha512-+VfSV7Akh1XUiDNXgqgY1cUP1i2vjI+BmlyXRfVz5AfV3jbpde8JTs5Q9sYgaoq5cWfuKfoZB/QkGOI+QcL1Tw==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.43.tgz#4b41f465a787f91cc4fe7dffa0dcabf655935a1a"
   integrity sha512-f+v8cInPEL1/SDP//CfSYzcDNgE4CY3xgDV81DWm3KAPWzhvxARrKxB1Pstf5mB56yAslJDxu7ryBUPX207EZA==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.48.tgz#822c1778495f7868e990d4da47ad7281df28fd15"
+  integrity sha512-cs0uOiRlPp6ymknDnjajCgvDMSsLw5mST2UXh+ZIrXTj2Ifyf2aAP3Iw4DiqgnyYLV2O/v/yWBJx+WfmKEpNLA==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.43.tgz#ca15934f5b46728dd9ac05270e783e7feaca9eaf"
   integrity sha512-5wZYMDGAL/K2pqkdIsW+I4IR41kyfHr/QshJcNpUfK3RjB3VQcPWOaZmc+74rm4ZjVirYrtz+jWw0SgxtxRanA==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.48.tgz#55de0a9ec4a48fedfe82a63e083164d001709447"
+  integrity sha512-+2F0vJMkuI0Wie/wcSPDCqXvSFEELH7Jubxb7mpWrA/4NpT+/byjxDz0gG6R1WJoeDefcrMfpBx4GFNN1JQorQ==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.43.tgz#70fce2b5a0605a67e58b5a357b0e00be1029836d"
   integrity sha512-lYcAOUxp85hC7lSjycJUVSmj4/9oEfSyXjb/ua9bNl8afonaduuqtw7hvKMoKuYnVwOCDw4RSfKpcnIRDWq+Bw==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.48.tgz#cd2b7381880b2f4b21a5a598fb673492120f18a5"
+  integrity sha512-BmaK/GfEE+5F2/QDrIXteFGKnVHGxlnK9MjdVKMTfvtmudjY3k2t8NtlY4qemKSizc+QwyombGWTBDc76rxePA==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.43.tgz#318d03b4f4ccc7fa44ac7562121cf4a4529e477a"
   integrity sha512-27e43ZhHvhFE4nM7HqtUbMRu37I/4eNSUbb8FGZWszV+uLzMIsHDwLoBiJmw7G9N+hrehNPeQ4F5Ujad0DrUKQ==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.48.tgz#4b319eca2a5c64637fc7397ffbd9671719cdb6bf"
+  integrity sha512-tndw/0B9jiCL+KWKo0TSMaUm5UWBLsfCKVdbfMlb3d5LeV9WbijZ8Ordia8SAYv38VSJWOEt6eDCdOx8LqkC4g==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.43.tgz#86130ce204ef0162a96e863b55851efecc92f423"
   integrity sha512-2mH4QF6hHBn5zzAfxEI/2eBC0mspVsZ6UVo821LpAJKMvLJPBk3XJO5xwg7paDqSqpl7p6IRrAenW999AEfJhQ==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.48.tgz#c27cde8b5cb55dcc227943a18ab078fb98d0adbf"
+  integrity sha512-V9hgXfwf/T901Lr1wkOfoevtyNkrxmMcRHyticybBUHookznipMOHoF41Al68QBsqBxnITCEpjjd4yAos7z9Tw==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.43.tgz#0229dc2db2ded97b03bb93bba7646b30ffdf5d0d"
   integrity sha512-ZhQpiZjvqCqO8jKdGp9+8k9E/EHSA+zIWOg+grwZasI9RoblqJ1QiZqqi7jfd6ZrrG1UFBNGe4m0NFxCFbMVbg==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.48.tgz#af5ab2d1cb41f09064bba9465fc8bf1309150df1"
+  integrity sha512-+IHf4JcbnnBl4T52egorXMatil/za0awqzg2Vy6FBgPcBpisDWT2sVz/tNdrK9kAqj+GZG/jZdrOkj7wsrNTKA==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.43.tgz#17e316216eb9f1de25d52a9000356ae5b869e736"
   integrity sha512-DgxSi9DaHReL9gYuul2rrQCAapgnCJkh3LSHPKsY26zytYppG0HgkgVF80zjIlvEsUbGBP/GHQzBtrezj/Zq1Q==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.48.tgz#db3ae20526055cf6fd5c4582676233814603ac54"
+  integrity sha512-77m8bsr5wOpOWbGi9KSqDphcq6dFeJyun8TA+12JW/GAjyfTwVtOnN8DOt6DSPUfEV+ltVMNqtXUeTeMAxl5KA==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.43.tgz#a173757bc6dfd0f2656ff40b64f7f9290745778e"
   integrity sha512-Ih3+2O5oExiqm0mY6YYE5dR0o8+AspccQ3vIAtRodwFvhuyGLjb0Hbmzun/F3Lw19nuhPMu3sW2fqIJ5xBxByw==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.48.tgz#021ffceb0a3f83078262870da88a912293c57475"
+  integrity sha512-EPgRuTPP8vK9maxpTGDe5lSoIBHGKO/AuxDncg5O3NkrPeLNdvvK8oywB0zGaAZXxYWfNNSHskvvDgmfVTguhg==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.43.tgz#c447b23126aad158c4fe6a394342cafd97926ed1"
   integrity sha512-8NsuNfI8xwFuJbrCuI+aBqNTYkrWErejFO5aYM+yHqyHuL8mmepLS9EPzAzk8rvfaJrhN0+RvKWAcymViHOKEw==
 
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.48.tgz#a4d3407b580f9faac51f61eec095fa985fb3fee4"
+  integrity sha512-YmpXjdT1q0b8ictSdGwH3M8VCoqPpK1/UArze3X199w6u8hUx3V8BhAi1WjbsfDYRBanVVtduAhh2sirImtAvA==
+
 [email protected]:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.43.tgz#3caed1b430d394d7a7836407b9d36c4750246e76"
   integrity sha512-7ZlD7bo++kVRblJEoG+cepljkfP8bfuTPz5fIXzptwnPaFwGS6ahvfoYzY7WCf5v/1nX2X02HDraVItTgbHnKw==
 
-esbuild@^0.14.25, esbuild@^0.14.27, esbuild@^0.14.43:
[email protected]:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.48.tgz#762c0562127d8b09bfb70a3c816460742dd82880"
+  integrity sha512-HHaOMCsCXp0rz5BT2crTka6MPWVno121NKApsGs/OIW5QC0ggC69YMGs1aJct9/9FSUF4A1xNE/cLvgB5svR4g==
+
+esbuild@^0.14.25, esbuild@^0.14.27:
   version "0.14.43"
   resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.43.tgz#c227d585c512d3e0f23b88f50b8e16501147f647"
   integrity sha512-Uf94+kQmy/5jsFwKWiQB4hfo/RkM9Dh7b79p8yqd1tshULdr25G2szLz631NoH3s2ujnKEKVD16RmOxvCNKRFA==
@@ -4084,6 +4061,32 @@ esbuild@^0.14.25, esbuild@^0.14.27, esbuild@^0.14.43:
     esbuild-windows-64 "0.14.43"
     esbuild-windows-arm64 "0.14.43"
 
+esbuild@^0.14.48:
+  version "0.14.48"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.48.tgz#da5d8d25cd2d940c45ea0cfecdca727f7aee2b85"
+  integrity sha512-w6N1Yn5MtqK2U1/WZTX9ZqUVb8IOLZkZ5AdHkT6x3cHDMVsYWC7WPdiLmx19w3i4Rwzy5LqsEMtVihG3e4rFzA==
+  optionalDependencies:
+    esbuild-android-64 "0.14.48"
+    esbuild-android-arm64 "0.14.48"
+    esbuild-darwin-64 "0.14.48"
+    esbuild-darwin-arm64 "0.14.48"
+    esbuild-freebsd-64 "0.14.48"
+    esbuild-freebsd-arm64 "0.14.48"
+    esbuild-linux-32 "0.14.48"
+    esbuild-linux-64 "0.14.48"
+    esbuild-linux-arm "0.14.48"
+    esbuild-linux-arm64 "0.14.48"
+    esbuild-linux-mips64le "0.14.48"
+    esbuild-linux-ppc64le "0.14.48"
+    esbuild-linux-riscv64 "0.14.48"
+    esbuild-linux-s390x "0.14.48"
+    esbuild-netbsd-64 "0.14.48"
+    esbuild-openbsd-64 "0.14.48"
+    esbuild-sunos-64 "0.14.48"
+    esbuild-windows-32 "0.14.48"
+    esbuild-windows-64 "0.14.48"
+    esbuild-windows-arm64 "0.14.48"
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -4413,11 +4416,6 @@ filter-obj@^1.1.0:
   resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b"
   integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==
 
-find-root@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
-  integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
-
 find-up@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
@@ -4810,13 +4808,6 @@ hmac-drbg@^1.0.1:
     minimalistic-assert "^1.0.0"
     minimalistic-crypto-utils "^1.0.1"
 
-hoist-non-react-statics@^3.3.1:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
-  integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
-  dependencies:
-    react-is "^16.7.0"
-
 hosted-git-info@^2.1.4:
   version "2.8.9"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@@ -4942,7 +4933,7 @@ ignore@^5.1.1, ignore@^5.1.4, ignore@^5.2.0:
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
   integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
 
-import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
+import-fresh@^3.0.0, import-fresh@^3.2.1:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
   integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -5917,7 +5908,7 @@ log-update@^4.0.0:
     slice-ansi "^4.0.0"
     wrap-ansi "^6.2.0"
 
-loose-envify@^1.1.0, loose-envify@^1.4.0:
+loose-envify@^1.1.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
   integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -6085,11 +6076,6 @@ mdast-util-to-string@^3.1.0:
   resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9"
   integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==
 
-memoize-one@^5.0.0:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
-  integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
-
 meow@^8.0.0:
   version "8.1.2"
   resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897"
@@ -7347,15 +7333,6 @@ promzard@^0.3.0:
   dependencies:
     read "1"
 
-prop-types@^15.6.0, prop-types@^15.6.2:
-  version "15.8.1"
-  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
-  integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
-  dependencies:
-    loose-envify "^1.4.0"
-    object-assign "^4.1.1"
-    react-is "^16.13.1"
-
 proto-list@~1.2.1:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -7491,11 +7468,6 @@ react-dom@^17, react-dom@^17.0.0:
     object-assign "^4.1.1"
     scheduler "^0.20.2"
 
-react-is@^16.13.1, react-is@^16.7.0:
-  version "16.13.1"
-  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
-  integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-
 react-is@^17.0.1:
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
@@ -7511,29 +7483,6 @@ react-refresh@^0.13.0:
   resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.13.0.tgz#cbd01a4482a177a5da8d44c9755ebb1f26d5a1c1"
   integrity sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==
 
-react-select@^5.3.2:
-  version "5.3.2"
-  resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.3.2.tgz#ecee0d5c59ed4acb7f567f7de3c75a488d93dacb"
-  integrity sha512-W6Irh7U6Ha7p5uQQ2ZnemoCQ8mcfgOtHfw3wuMzG6FAu0P+CYicgofSLOq97BhjMx8jS+h+wwWdCBeVVZ9VqlQ==
-  dependencies:
-    "@babel/runtime" "^7.12.0"
-    "@emotion/cache" "^11.4.0"
-    "@emotion/react" "^11.8.1"
-    "@types/react-transition-group" "^4.4.0"
-    memoize-one "^5.0.0"
-    prop-types "^15.6.0"
-    react-transition-group "^4.3.0"
-
-react-transition-group@^4.3.0:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
-  integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
-  dependencies:
-    "@babel/runtime" "^7.5.5"
-    dom-helpers "^5.0.1"
-    loose-envify "^1.4.0"
-    prop-types "^15.6.2"
-
 react@^17, react@^17.0.0:
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
@@ -7744,7 +7693,7 @@ resolve.exports@^1.1.0:
   resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9"
   integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==
 
-resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.20.0, resolve@^1.22.0:
+resolve@^1.1.7, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.0:
   version "1.22.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
   integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
@@ -7894,10 +7843,10 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
   integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
 
-shadow-cljs@^2.19.3:
-  version "2.19.3"
-  resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.19.3.tgz#115a33917f8bca1495e0f815dca7ec3957f669af"
-  integrity sha512-9TsTCRlmR8m1g2ekwblgomRUgJpbifQI99VlRrlH9NMqEzklev3zYAD1dvy4d5h8BoAhgdxOOEg7ld2d45CWTA==
+shadow-cljs@^2.19.5:
+  version "2.19.5"
+  resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.19.5.tgz#e51c758d2f942db18e6e4015bcacf1857ad1e751"
+  integrity sha512-uZelOtmTYg4MOZP1ehJilhQcGDxkdybPKkGZ11qxp8awmfgAQMe+W/QEyZw4aVwFxVXyHIIerzCGkCqAgo/FuA==
   dependencies:
     node-libs-browser "^2.2.1"
     readline-sync "^1.4.7"
@@ -8091,7 +8040,7 @@ [email protected]:
   dependencies:
     whatwg-url "^7.0.0"
 
-source-map@^0.5.6, source-map@^0.5.7:
+source-map@^0.5.6:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
@@ -8331,11 +8280,6 @@ strong-log-transformer@^2.1.0:
     minimist "^1.2.0"
     through "^2.3.4"
 
[email protected]:
-  version "4.0.13"
-  resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
-  integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
-
 sucrase@^3.20.3:
   version "3.21.0"
   resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.21.0.tgz#6a5affdbe716b22e4dc99c57d366ad0d216444b9"
@@ -9055,7 +8999,7 @@ yallist@^4.0.0:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
-yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
+yaml@^1.10.0, yaml@^1.10.2:
   version "1.10.2"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
   integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==