Peng Xiao 3 lat temu
rodzic
commit
97120441db
100 zmienionych plików z 18148 dodań i 99 usunięć
  1. 2 0
      .carve/ignore
  2. 1 0
      .clj-kondo/babashka/fs/config.edn
  3. 5 0
      .clj-kondo/rewrite-clj/rewrite-clj/config.edn
  4. 2 0
      package.json
  5. 279 0
      resources/css/tldraw.css
  6. 12 2
      shadow-cljs.edn
  7. 1 1
      src/electron/electron/handler.cljs
  8. 4 10
      src/main/frontend/commands.cljs
  9. 22 1
      src/main/frontend/components/block.cljs
  10. 1 0
      src/main/frontend/components/editor.cljs
  11. 10 1
      src/main/frontend/components/page.cljs
  12. 13 7
      src/main/frontend/components/sidebar.cljs
  13. 1 1
      src/main/frontend/components/sidebar.css
  14. 1 1
      src/main/frontend/config.cljs
  15. 68 0
      src/main/frontend/extensions/draw.cljs
  16. 7 64
      src/main/frontend/extensions/excalidraw.cljs
  17. 52 0
      src/main/frontend/extensions/tldraw.cljs
  18. 1 1
      src/main/frontend/format/block.cljs
  19. 32 9
      src/main/frontend/handler/draw.cljs
  20. 3 0
      src/main/frontend/handler/editor.cljs
  21. 2 0
      src/main/frontend/handler/search.cljs
  22. 1 1
      src/main/frontend/mixins.cljs
  23. 5 0
      src/main/frontend/routes.cljs
  24. 2 0
      src/main/frontend/state.cljs
  25. 2 0
      src/main/frontend/util.cljc
  26. 1 0
      tailwind.all.css
  27. 3 0
      tldraw/tldraw-logseq/README.md
  28. 17 0
      tldraw/tldraw-logseq/build.mjs
  29. 269 0
      tldraw/tldraw-logseq/dist/index.css
  30. 264 0
      tldraw/tldraw-logseq/dist/index.js
  31. 14127 0
      tldraw/tldraw-logseq/dist/index.mjs
  32. 38 0
      tldraw/tldraw-logseq/dist/package.json
  33. 38 0
      tldraw/tldraw-logseq/package.json
  34. 111 0
      tldraw/tldraw-logseq/src/app.tsx
  35. 15 0
      tldraw/tldraw-logseq/src/components/AppUI.tsx
  36. 9 0
      tldraw/tldraw-logseq/src/components/Button/Button.tsx
  37. 1 0
      tldraw/tldraw-logseq/src/components/Button/index.ts
  38. 143 0
      tldraw/tldraw-logseq/src/components/ContextBar/ContextBar.tsx
  39. 1 0
      tldraw/tldraw-logseq/src/components/ContextBar/index.ts
  40. 145 0
      tldraw/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx
  41. 1 0
      tldraw/tldraw-logseq/src/components/PrimaryTools/index.ts
  42. 15 0
      tldraw/tldraw-logseq/src/components/StatusBar/StatusBar.tsx
  43. 1 0
      tldraw/tldraw-logseq/src/components/StatusBar/index.ts
  44. 67 0
      tldraw/tldraw-logseq/src/components/Toolbar/ToolBar.tsx
  45. 1 0
      tldraw/tldraw-logseq/src/components/Toolbar/index.ts
  46. 25 0
      tldraw/tldraw-logseq/src/components/icons/BoxIcon.tsx
  47. 14 0
      tldraw/tldraw-logseq/src/components/icons/CircleIcon.tsx
  48. 17 0
      tldraw/tldraw-logseq/src/components/icons/DashDashedIcon.tsx
  49. 19 0
      tldraw/tldraw-logseq/src/components/icons/DashDottedIcon.tsx
  50. 19 0
      tldraw/tldraw-logseq/src/components/icons/DashDrawIcon.tsx
  51. 9 0
      tldraw/tldraw-logseq/src/components/icons/DashSolidIcon.tsx
  52. 15 0
      tldraw/tldraw-logseq/src/components/icons/DiscordIcon.tsx
  53. 21 0
      tldraw/tldraw-logseq/src/components/icons/EraserIcon.tsx
  54. 14 0
      tldraw/tldraw-logseq/src/components/icons/HeartIcon.tsx
  55. 18 0
      tldraw/tldraw-logseq/src/components/icons/IsFilledIcon.tsx
  56. 15 0
      tldraw/tldraw-logseq/src/components/icons/LineIcon.tsx
  57. 14 0
      tldraw/tldraw-logseq/src/components/icons/MultiplayerIcon.tsx
  58. 16 0
      tldraw/tldraw-logseq/src/components/icons/RedoIcon.tsx
  59. 16 0
      tldraw/tldraw-logseq/src/components/icons/SizeLargeIcon.tsx
  60. 16 0
      tldraw/tldraw-logseq/src/components/icons/SizeMediumIcon.tsx
  61. 16 0
      tldraw/tldraw-logseq/src/components/icons/SizeSmallIcon.tsx
  62. 30 0
      tldraw/tldraw-logseq/src/components/icons/TrashIcon.tsx
  63. 16 0
      tldraw/tldraw-logseq/src/components/icons/UndoIcon.tsx
  64. 17 0
      tldraw/tldraw-logseq/src/components/icons/index.ts
  65. 14 0
      tldraw/tldraw-logseq/src/components/inputs/ColorInput.tsx
  66. 14 0
      tldraw/tldraw-logseq/src/components/inputs/NumberInput.tsx
  67. 16 0
      tldraw/tldraw-logseq/src/components/inputs/TextInput.tsx
  68. 35 0
      tldraw/tldraw-logseq/src/documents/dev.ts
  69. 18 0
      tldraw/tldraw-logseq/src/documents/empty.ts
  70. 27 0
      tldraw/tldraw-logseq/src/documents/withAsset.ts
  71. 149 0
      tldraw/tldraw-logseq/src/documents/withEverything.ts
  72. 55 0
      tldraw/tldraw-logseq/src/hooks/useFileDrop.ts
  73. 10 0
      tldraw/tldraw-logseq/src/index.ts
  74. 3 0
      tldraw/tldraw-logseq/src/lib/index.ts
  75. 7 0
      tldraw/tldraw-logseq/src/lib/logseq-context.ts
  76. 85 0
      tldraw/tldraw-logseq/src/lib/shapes/BoxShape.tsx
  77. 122 0
      tldraw/tldraw-logseq/src/lib/shapes/CodeSandboxShape.tsx
  78. 53 0
      tldraw/tldraw-logseq/src/lib/shapes/DotShape.tsx
  79. 74 0
      tldraw/tldraw-logseq/src/lib/shapes/EllipseShape.tsx
  80. 71 0
      tldraw/tldraw-logseq/src/lib/shapes/HighlighterShape.tsx
  81. 78 0
      tldraw/tldraw-logseq/src/lib/shapes/ImageShape.tsx
  82. 64 0
      tldraw/tldraw-logseq/src/lib/shapes/LineShape.tsx
  83. 182 0
      tldraw/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx
  84. 76 0
      tldraw/tldraw-logseq/src/lib/shapes/PenShape.tsx
  85. 65 0
      tldraw/tldraw-logseq/src/lib/shapes/PencilShape.tsx
  86. 69 0
      tldraw/tldraw-logseq/src/lib/shapes/PolygonShape.tsx
  87. 60 0
      tldraw/tldraw-logseq/src/lib/shapes/PolylineShape.tsx
  88. 75 0
      tldraw/tldraw-logseq/src/lib/shapes/StarShape.tsx
  89. 272 0
      tldraw/tldraw-logseq/src/lib/shapes/TextShape.tsx
  90. 186 0
      tldraw/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx
  91. 46 0
      tldraw/tldraw-logseq/src/lib/shapes/index.ts
  92. 24 0
      tldraw/tldraw-logseq/src/lib/shapes/style-props.tsx
  93. 9 0
      tldraw/tldraw-logseq/src/lib/tools/BoxTool.tsx
  94. 9 0
      tldraw/tldraw-logseq/src/lib/tools/CodeSandboxTool.tsx
  95. 9 0
      tldraw/tldraw-logseq/src/lib/tools/DotTool.tsx
  96. 9 0
      tldraw/tldraw-logseq/src/lib/tools/EllipseTool.tsx
  97. 8 0
      tldraw/tldraw-logseq/src/lib/tools/EraseTool.tsx
  98. 11 0
      tldraw/tldraw-logseq/src/lib/tools/HighlighterTool.tsx
  99. 9 0
      tldraw/tldraw-logseq/src/lib/tools/LineTool.tsx
  100. 11 0
      tldraw/tldraw-logseq/src/lib/tools/LogseqPortalTool.tsx

+ 2 - 0
.carve/ignore

@@ -22,6 +22,8 @@ frontend.extensions.age-encryption/encrypt-with-user-passphrase
 frontend.extensions.age-encryption/decrypt-with-user-passphrase
 ;; Lazily loaded
 frontend.extensions.excalidraw/draw
+;; Lazily loaded
+frontend.extensions.tldraw/draw
 ;; Referenced in commented TODO
 frontend.extensions.pdf.utils/get-page-bounding
 ;; For repl

+ 1 - 0
.clj-kondo/babashka/fs/config.edn

@@ -0,0 +1 @@
+{:lint-as {babashka.fs/with-temp-dir clojure.core/let}}

+ 5 - 0
.clj-kondo/rewrite-clj/rewrite-clj/config.edn

@@ -0,0 +1,5 @@
+{:lint-as
+ {rewrite-clj.zip/subedit-> clojure.core/->
+  rewrite-clj.zip/subedit->> clojure.core/->>
+  rewrite-clj.zip/edit-> clojure.core/->
+  rewrite-clj.zip/edit->> clojure.core/->>}}

+ 2 - 0
package.json

@@ -81,6 +81,7 @@
         "@sentry/tracing": "^6.18.2",
         "@tabler/icons": "1.54.0",
         "@tippyjs/react": "4.2.5",
+        "@tldraw/tldraw": "1.10.0",
         "capacitor-voice-recorder": "2.1.0",
         "chokidar": "3.5.1",
         "chrono-node": "2.2.4",
@@ -100,6 +101,7 @@
         "is-svg": "4.3.0",
         "jszip": "3.5.0",
         "mldoc": "1.3.3",
+        "mobx": "^6.5.0",
         "path": "0.12.7",
         "pixi-graph-fork": "0.2.0",
         "pixi.js": "6.2.0",

+ 279 - 0
resources/css/tldraw.css

@@ -0,0 +1,279 @@
+@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;
+}

+ 12 - 2
shadow-cljs.edn

@@ -5,6 +5,8 @@
  ;; "." for /static
  :dev-http {3001 ["static" "."]
             3002 ["public/workspaces" "."]}
+ 
+ :js-options {:js-package-dirs ["node_modules" "tldraw"]}
 
  :builds
  {:app {:target        :browser
@@ -20,7 +22,11 @@
                          :depends-on #{:main}}
                         :excalidraw
                         {:entries    [frontend.extensions.excalidraw]
-                         :depends-on #{:main}}}
+                         :depends-on #{:main}}
+                     ;;    :tldraw
+                     ;;    {:entries    [frontend.extensions.tldraw]
+                     ;;     :depends-on #{:main}}
+                        }
         :output-dir       "./static/js"
         :asset-path       "/static/js"
         :release          {:asset-path "https://asset.logseq.com/static/js"}
@@ -77,7 +83,11 @@
                                 :depends-on #{:main}}
                                :excalidraw
                                {:entries    [frontend.extensions.excalidraw]
-                                :depends-on #{:main}}}
+                                :depends-on #{:main}}
+                            ;;    :tldraw
+                            ;;    {:entries    [frontend.extensions.tldraw]
+                            ;;     :depends-on #{:main}}
+                               }
                :output-dir       "./static/js/publishing"
                :asset-path       "static/js"
                :closure-defines  {frontend.config/PUBLISHING true

+ 1 - 1
src/electron/electron/handler.cljs

@@ -149,7 +149,7 @@
   (fs/statSync path))
 
 (defonce allowed-formats
-  #{:org :markdown :md :edn :json :js :css :excalidraw})
+  #{:org :markdown :md :edn :json :js :css :excalidraw :tldraw})
 
 (defn get-ext
   [p]

+ 4 - 10
src/main/frontend/commands.cljs

@@ -4,7 +4,7 @@
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db.utils :as db-util]
-            [frontend.handler.draw :as draw]
+            [frontend.handler.draw :as draw-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.extensions.video.youtube :as youtube]
@@ -18,8 +18,7 @@
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.config :as gp-config]
             [goog.dom :as gdom]
-            [goog.object :as gobj]
-            [promesa.core :as p]))
+            [goog.object :as gobj]))
 
 ;; TODO: move to frontend.handler.editor.commands
 
@@ -274,13 +273,8 @@
      ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
      ["Calculator" [[:editor/input "```calc\n\n```" {:backward-pos 4}]
                     [:codemirror/focus]] "Insert a calculator"]
-     ["Draw" (fn []
-               (let [file (draw/file-name)
-                     path (str gp-config/default-draw-directory "/" file)
-                     text (util/format "[[%s]]" path)]
-                 (p/let [_ (draw/create-draw-with-default-content path)]
-                   (println "draw file created, " path))
-                 text)) "Draw a graph with Excalidraw"]
+     ["draw" (draw-handler/initialize-excalidarw-file) "Draw a graph with Excalidraw"]
+     ["tldraw" (draw-handler/initialize-tldraw-file) "Draw a graph with tldraw"]
 
      (when (util/zh-CN-supported?)
        ["Embed Bilibili video" [[:editor/input "{{bilibili }}" {:last-pattern (state/get-editor-command-trigger)

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

@@ -541,16 +541,37 @@
     (when draw-component
       (draw-component {:file file :block-uuid block-uuid}))))
 
+(defonce tldraw-loaded? (atom false))
+(rum/defc tldraw < rum/reactive
+  {:init (fn [state]
+           (p/let [_ (loader/load :tldraw)]
+             (reset! tldraw-loaded? true))
+           state)}
+  [file block-uuid]
+  (let [loaded? (rum/react tldraw-loaded?)
+        draw-component (when loaded?
+                         (resolve 'frontend.extensions.tldraw/draw))]
+    (when draw-component
+      (draw-component {:file file :block-uuid block-uuid}))))
+
 (rum/defc page-reference < rum/reactive
   [html-export? s config label]
   (let [show-brackets? (state/show-brackets?)
         nested-link? (:nested-link? config)
         contents-page? (= "contents" (string/lower-case (str (:id config))))
         block-uuid (:block/uuid config)]
-    (if (string/ends-with? s ".excalidraw")
+    (cond
+      (string/ends-with? s ".excalidraw")
       [:div.draw {:on-click (fn [e]
                               (.stopPropagation e))}
        (excalidraw s block-uuid)]
+
+      (string/ends-with? s ".tldr")
+      [:div.draw.cursor-default {:on-click (fn [e]
+                                             (.stopPropagation e))}
+       (tldraw s block-uuid)]
+
+      :else
       [:span.page-reference
        {:data-ref s}
        (when (and (or show-brackets? nested-link?)

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

@@ -466,6 +466,7 @@
   (let [{:keys [id format]} (get-state)
         input-id id
         input (gdom/getElement input-id)]
+    (println "setup-key-listener!" input id)
     (set-up-key-down! state format)
     (set-up-key-up! state input input-id search-timeout)))
 

+ 10 - 1
src/main/frontend/components/page.cljs

@@ -15,6 +15,8 @@
             [frontend.db.model :as model]
             [frontend.extensions.graph :as graph]
             [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.modules.shortcut.core :as shortcut]
+            [frontend.handler.editor.lifecycle :as lifecycle]
             [frontend.format.block :as format-block]
             [frontend.handler.common :as common-handler]
             [frontend.handler.config :as config-handler]
@@ -69,7 +71,13 @@
 
 (rum/defc page-blocks-inner <
   {:did-mount  open-first-block!
-   :did-update open-first-block!}
+   :did-update open-first-block!
+   :should-update (fn [prev-state state]
+                    (let [[old-page-name _ old-hiccup _ old-block-uuid] (:rum/args prev-state)
+                          [page-name _ hiccup _ block-uuid] (:rum/args state)]
+                      (or (not= page-name old-page-name)
+                          (not= hiccup old-hiccup)
+                          (not= block-uuid old-block-uuid))))}
   [page-name _blocks hiccup sidebar? _block-uuid]
   [:div.page-blocks-inner {:style {:margin-left (if sidebar? 0 -20)}}
    (rum/with-key
@@ -305,6 +313,7 @@
   (rum/local false ::control-show?)
   [state {:keys [repo page-name] :as option}]
   (when-let [path-page-name (or page-name
+                                (gobj/get option "pageId")
                                 (get-page-name state)
                                 (state/get-current-page))]
     (let [current-repo (state/sub :git/current-repo)

+ 13 - 7
src/main/frontend/components/sidebar.cljs

@@ -241,6 +241,12 @@
          {:class "all-pages-nav"
           :title (t :right-side-bar/all-pages)
           :href  (rfe/href :all-pages)
+          :icon  "files"})
+        
+        (sidebar-item
+         {:class "whiteboard"
+          :title "Whiteboard"
+          :href  (rfe/href :whiteboard)
           :icon  "files"})]]
 
       (favorites t)
@@ -279,7 +285,7 @@
                               (let [format (:block/format (state/get-edit-block))]
                                 (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))}))
                 state)}
-  [{:keys [route-match global-graph-pages? route-name indexeddb-support? db-restoring? main-content]}]
+  [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content]}]
   (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
                                   (not config/publishing?)
@@ -294,8 +300,8 @@
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
 
       [:div.cp__sidebar-main-content
-       {:data-is-global-graph-pages global-graph-pages?
-        :data-is-full-width         (or global-graph-pages?
+       {:data-is-margin-less-pages margin-less-pages?
+        :data-is-full-width        (or margin-less-pages?
                                         (contains? #{:all-files :all-pages :my-publishing} route-name))}
 
        (when (and (not (mobile-util/is-native-platform?))
@@ -312,9 +318,9 @@
            (ui/loading (t :loading))]]
 
          :else
-         [:div {:class (if global-graph-pages? "" (util/hiccup->class "max-w-7xl.mx-auto.pb-24"))
+         [:div {:class (if margin-less-pages? "" (util/hiccup->class "max-w-7xl.mx-auto.pb-24"))
                 :style {:margin-bottom (cond
-                                         global-graph-pages? 0
+                                         margin-less-pages? 0
                                          onboarding-and-home? -48
                                          :else 120)
                         :padding-bottom (when (mobile-util/native-iphone?) "7rem")}}
@@ -472,7 +478,7 @@
         wide-mode? (state/sub :ui/wide-mode?)
         right-sidebar-blocks (state/sub-right-sidebar-blocks)
         route-name (get-in route-match [:data :name])
-        global-graph-pages? (= :graph route-name)
+        margin-less-pages? (#{:graph :whiteboard} route-name)
         db-restoring? (state/sub :db/restoring?)
         indexeddb-support? (state/sub :indexeddb/support?)
         page? (= :page route-name)
@@ -514,7 +520,7 @@
                         :new-block-mode new-block-mode})
 
         (main {:route-match         route-match
-               :global-graph-pages? global-graph-pages?
+               :margin-less-pages?  margin-less-pages?
                :logged?             logged?
                :home?               home?
                :route-name          route-name

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

@@ -455,7 +455,7 @@ html[data-theme='dark'] {
   max-width: 100vw;
 }
 
-.cp__sidebar-main-content[data-is-global-graph-pages='true'] {
+.cp__sidebar-main-content[data-is-margin-less-pages='true'] {
   padding: 0;
 }
 

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

@@ -70,7 +70,7 @@
     (set/union
      config-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 :tldraw})))
 
 (def markup-formats
   #{:org :md :markdown :asciidoc :adoc :rst})

+ 68 - 0
src/main/frontend/extensions/draw.cljs

@@ -0,0 +1,68 @@
+(ns frontend.extensions.draw
+  (:require [clojure.string :as string]
+            [frontend.config :as config]
+            [frontend.handler.draw :as draw-handler]
+            [frontend.handler.notification :as notification]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [rum.core :as rum]))
+
+
+(defn- from-json
+  [text]
+  (when-not (string/blank? text)
+    (try
+      (js/JSON.parse text)
+      (catch js/Error e
+        (println "from json error:")
+        (js/console.dir e)
+        (notification/show!
+         (util/format "Could not load this invalid excalidraw file")
+         :error)))))
+
+(rum/defcs draw-container < rum/reactive
+  {:init (fn [state]
+           (let [[option] (:rum/args state)
+                 file (:file option)
+                 *data (atom nil)
+                 *loading? (atom true)]
+             (when file
+               (draw-handler/load-draw-file
+                file
+                (fn [data]
+                  (let [data (from-json data)]
+                    (reset! *data data)
+                    (reset! *loading? false)))))
+             (assoc state
+                    ::data *data
+                    ::loading? *loading?)))}
+  [state option draw-inner]
+  (let [*data (get state ::data)
+        *loading? (get state ::loading?)
+        loading? (rum/react *loading?)
+        data (rum/react *data)
+        db-restoring? (state/sub :db/restoring?)]
+    (when (:file option)
+      (cond
+        db-restoring?
+        [:div.ls-center
+         (ui/loading "Loading")]
+
+        (false? loading?)
+        (draw-inner data option)
+
+        :else
+        nil))))
+
+(rum/defc draw-wrapper < rum/reactive
+  [option draw-inner]
+  (let [repo (state/get-current-repo)
+        granted? (state/sub [:nfs/user-granted? repo])]
+    ;; Web granted
+    (when-not (and (config/local-db? repo)
+                   (not granted?)
+                   (not (util/electron?))
+                   (not (mobile-util/is-native-platform?)))
+      (draw-container option draw-inner))))

+ 7 - 64
src/main/frontend/extensions/excalidraw.cljs

@@ -1,38 +1,22 @@
 (ns frontend.extensions.excalidraw
   (:require [cljs-bean.core :as bean]
-            [clojure.string :as string]
             ;; NOTE: Always use production build of excalidraw
             ;; See-also: https://github.com/excalidraw/excalidraw/pull/3330
             ["@excalidraw/excalidraw/dist/excalidraw.production.min" :as Excalidraw]
-            [frontend.config :as config]
             [frontend.db :as db]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.draw :as draw]
-            [frontend.handler.notification :as notification]
+            [frontend.handler.draw :as draw-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.rum :as r]
             [frontend.state :as state]
-            [frontend.ui :as ui]
             [frontend.util :as util]
+            [frontend.extensions.draw :as draw-common]
             [goog.object :as gobj]
-            [rum.core :as rum]
-            [frontend.mobile.util :as mobile-util]))
+            [rum.core :as rum]))
 
 (def excalidraw (r/adapt-class (gobj/get Excalidraw "default")))
 (def serialize-as-json (gobj/get Excalidraw "serializeAsJSON"))
 
-(defn from-json
-  [text]
-  (when-not (string/blank? text)
-    (try
-      (js/JSON.parse text)
-      (catch js/Error e
-        (println "from json error:")
-        (js/console.dir e)
-        (notification/show!
-         (util/format "Could not load this invalid excalidraw file")
-         :error)))))
-
 (defn- update-draw-content-width
   [state]
   (when-let [el ^js (rum/dom-node state)]
@@ -98,56 +82,15 @@
                             (when (and (seq elements->clj)
                                        (not= elements->clj @*elements)) ;; not= requires clj collections
                               (reset! *elements elements->clj)
-                              (draw/save-excalidraw!
+                              (draw-handler/save-draw!
                                file
                                (serialize-as-json elements app-state))))))
-           
+
            :zen-mode-enabled @*zen-mode?
            :view-mode-enabled @*view-mode?
            :grid-mode-enabled @*grid-mode?
            :initial-data data}))]])))
 
-(rum/defcs draw-container < rum/reactive
-  {:init (fn [state]
-           (let [[option] (:rum/args state)
-                 file (:file option)
-                 *data (atom nil)
-                 *loading? (atom true)]
-             (when file
-               (draw/load-excalidraw-file
-                file
-                (fn [data]
-                  (let [data (from-json data)]
-                    (reset! *data data)
-                    (reset! *loading? false)))))
-             (assoc state
-                    ::data *data
-                    ::loading? *loading?)))}
-  [state option]
-  (let [*data (get state ::data)
-        *loading? (get state ::loading?)
-        loading? (rum/react *loading?)
-        data (rum/react *data)
-        db-restoring? (state/sub :db/restoring?)]
-    (when (:file option)
-      (cond
-        db-restoring?
-        [:div.ls-center
-         (ui/loading "Loading")]
-
-        (false? loading?)
-        (draw-inner data option)
-
-        :else
-        nil))))
-
-(rum/defc draw < rum/reactive
+(rum/defc draw
   [option]
-  (let [repo (state/get-current-repo)
-        granted? (state/sub [:nfs/user-granted? repo])]
-    ;; Web granted
-    (when-not (and (config/local-db? repo)
-                   (not granted?)
-                   (not (util/electron?))
-                   (not (mobile-util/is-native-platform?)))
-      (draw-container option))))
+  (draw-common/draw-wrapper option draw-inner))

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

@@ -0,0 +1,52 @@
+(ns frontend.extensions.tldraw
+  (:require ["tldraw-logseq$App" :as tldraw-app]
+            [frontend.components.page :refer [page]]
+            [frontend.extensions.draw :as draw-common]
+            [frontend.handler.draw :as draw-handler]
+            [frontend.search :as search]
+            [frontend.rum :as r]
+            [frontend.state :as state]
+            [goog.object :as gobj]
+            [rum.core :as rum]))
+
+(def tldraw (r/adapt-class tldraw-app))
+
+;; from apps/logseq/src/documents/dev.ts
+(def dev-doc-model
+  {:currentPageId "page1",
+   :selectedIds [],
+   :pages
+   [{:name "Page",
+     :id "page1",
+     :shapes
+     [{:id "logseq-portal-1",
+       :type "logseq-portal",
+       :parentId "page1",
+       :point [100 100],
+       :size [160 90],
+       :pageId "asdfasdf"}],
+     :bindings []}],
+   :assets []})
+
+(rum/defcs draw-inner < rum/reactive
+  (rum/local false ::view-mode?)
+  [state data option]
+  (let [{:keys [file]} option]
+    (when file
+      [:div.overflow-hidden.draw
+       {:style {:overscroll-behavior "none"}}
+       [:div.draw-wrap.relative
+        {:on-blur #(state/set-block-component-editing-mode! false)
+         :style {:height "calc(100vh - 120px)" }}
+
+        (tldraw {:PageComponent page
+                 :searchHandler (comp clj->js vec search/page-search)
+                 :onPersist (fn [app]
+                              (let [document (gobj/get app "serialized")
+                                    s (js/JSON.stringify document)]
+                                (draw-handler/save-draw! file s)))
+                 :model data})]])))
+
+(rum/defc draw
+  [option]
+  (draw-common/draw-wrapper option draw-inner))

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

@@ -72,7 +72,7 @@
                                 (not (util/starts-with? value "https:"))
                                 (not (util/starts-with? value "file:"))
                                 (not (gp-config/local-asset? value))
-                                (or (= ext :excalidraw)
+                                (or (some #{ext} [:excalidraw :tldraw])
                                     (not (contains? (config/supported-formats) ext))))
                        value)))
 

+ 32 - 9
src/main/frontend/handler/draw.cljs

@@ -19,7 +19,7 @@
        (fn [_result] nil)
        (fn [_error] nil)))))
 
-(defn save-excalidraw!
+(defn save-draw!
   [file data]
   (let [path file
         repo (state/get-current-repo)]
@@ -38,7 +38,7 @@
                     (prn "Write file failed, path: " path ", data: " data)
                     (js/console.dir error))))))))
 
-(defn load-excalidraw-file
+(defn load-draw-file
   [file ok-handler]
   (when-let [repo (state/get-current-repo)]
     (util/p-handle
@@ -49,19 +49,42 @@
        (println "Error loading " file ": "
                 error)))))
 
-(defonce default-content
+(defonce default-excalidraw-content
   (util/format
    "{\n  \"type\": \"excalidraw\",\n  \"version\": 2,\n  \"source\": \"%s\",\n  \"elements\": [],\n  \"appState\": {\n    \"viewBackgroundColor\": \"#FFF\",\n    \"gridSize\": null\n  }\n}"
    config/website))
 
-(defn file-name
-  []
-  (str (date/get-date-time-string-2) ".excalidraw"))
+(defonce default-tldraw-content
+  (util/format
+   ""
+   config/website))
+
+(defn- file-name
+  [ext]
+  (str (date/get-date-time-string-2) ext))
 
-(defn create-draw-with-default-content
-  [current-file]
+(defn- create-draw-with-default-content
+  [current-file content]
   (when-let [repo (state/get-current-repo)]
     (p/let [exists? (fs/file-exists? (config/get-repo-dir repo)
                                      (str gp-config/default-draw-directory current-file))]
       (when-not exists?
-        (save-excalidraw! current-file default-content)))))
+        (save-draw! current-file content)))))
+
+(defn initialize-excalidarw-file
+  []
+  (let [file (file-name ".excalidraw")
+        path (str gp-config/default-draw-directory "/" file)
+        text (util/format "[[%s]]" path)]
+    (p/let [_ (create-draw-with-default-content path default-excalidraw-content)]
+      (println "excalidraw file created, " path))
+    text))
+
+(defn initialize-tldraw-file
+  []
+  (let [file (file-name ".tldr")
+        path (str gp-config/default-draw-directory "/" file)
+        text (util/format "[[%s]]" path)]
+    (p/let [_ (create-draw-with-default-content path default-tldraw-content)]
+      (println "tldraw file created, " path))
+    text))

+ 3 - 0
src/main/frontend/handler/editor.cljs

@@ -2264,6 +2264,7 @@
 
 (defn- keydown-new-block
   [state]
+  (println "keydown-new-block")
   (when-not (auto-complete?)
     (let [{:keys [block config]} (get-state)]
       (when block
@@ -2323,6 +2324,7 @@
                  (insert-new-block! state)))))))))
 
 (defn keydown-new-block-handler [state e]
+  (println "keydown-new-block-handler")
   (if (state/doc-mode-enter-for-new-line?)
     (keydown-new-line)
     (do
@@ -2330,6 +2332,7 @@
       (keydown-new-block state))))
 
 (defn keydown-new-line-handler [state e]
+  (println "keydown-new-line-handler")
   (if (state/doc-mode-enter-for-new-line?)
     (keydown-new-block state)
     (do

+ 2 - 0
src/main/frontend/handler/search.cljs

@@ -26,6 +26,8 @@
        (property/remove-built-in-properties format)))
 
 (defn search
+  ([q]
+   (search (state/get-current-repo) q))
   ([repo q]
    (search repo q {:limit 20}))
   ([repo q {:keys [page-db-id limit more?]

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

@@ -100,7 +100,7 @@
   ([state keycode-map]
    (on-key-down state keycode-map {}))
   ([state keycode-map {:keys [not-matched-handler all-handler target]}]
-   (listen state (or target js/window) "keydown"
+   (listen state (or target (.. js/window -document -documentElement)) "keydown"
            (fn [e]
              (let [key-code (.-keyCode e)]
                (if-let [f (get keycode-map key-code)]

+ 5 - 0
src/main/frontend/routes.cljs

@@ -9,6 +9,7 @@
             [frontend.components.settings :as settings]
             [frontend.components.shortcut :as shortcut]
             [frontend.components.onboarding.setups :as setups]
+            [frontend.extensions.tldraw :as tldraw]
             [frontend.extensions.zotero :as zotero]))
 
 ;; http://localhost:3000/#?anchor=fn.1
@@ -21,6 +22,10 @@
     {:name :repos
      :view repo/repos}]
 
+   ["/whiteboard"
+    {:name :whiteboard
+     :view #(tldraw/draw {:file "draws/2022-05-15-01-47-49.tldr"})}]
+
    ["/repo/add"
     {:name :repo-add
      :view setups/picker}]

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

@@ -821,6 +821,7 @@
   ([edit-input-id content block cursor-range]
    (set-editing! edit-input-id content block cursor-range true))
   ([edit-input-id content block cursor-range move-cursor?]
+   (println "set-editing!")
    (when (and edit-input-id block
               (or
                 (publishing-enable-editing?)
@@ -854,6 +855,7 @@
 
 (defn clear-edit!
   []
+  (println "clear-edit!")
   (swap! state merge {:editor/editing? nil
                       :editor/block    nil
                       :cursor-range    nil

+ 2 - 0
src/main/frontend/util.cljc

@@ -327,10 +327,12 @@
 
 #?(:cljs
    (defn stop [e]
+     (println "event stop" e)
      (when e (doto e (.preventDefault) (.stopPropagation)))))
 
 #?(:cljs
    (defn stop-propagation [e]
+     (println "event stop-propagation" e)
      (when e (.stopPropagation e))))
 
 #?(:cljs

+ 1 - 0
tailwind.all.css

@@ -7,6 +7,7 @@
 @import "resources/css/photoswipe.css";
 @import "resources/css/fonts.css";
 @import "resources/css/excalidraw.min.css";
+@import "resources/css/tldraw.css";
 @import "resources/css/katex.min.css";
 @import "resources/css/codemirror.min.css";
 @import "resources/css/codemirror.solarized.css";

+ 3 - 0
tldraw/tldraw-logseq/README.md

@@ -0,0 +1,3 @@
+# @tldraw/core Simple Example
+
+A (relatively) simple example project for `@tldraw/core`.

+ 17 - 0
tldraw/tldraw-logseq/build.mjs

@@ -0,0 +1,17 @@
+#!/usr/bin/env zx
+/* eslint-disable no-undef */
+import 'zx/globals'
+import fs from 'fs'
+
+// Build with [tsup](https://tsup.egoist.sh)
+await $`tsup`
+
+// Prepare package.json file
+const packageJson = fs.readFileSync('package.json', 'utf8')
+const glob = JSON.parse(packageJson)
+Object.assign(glob, {
+  main: './index.js',
+  module: './index.mjs'
+})
+
+fs.writeFileSync('dist/package.json', JSON.stringify(glob, null, 2))

+ 269 - 0
tldraw/tldraw-logseq/dist/index.css

@@ -0,0 +1,269 @@
+@import "https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap";
+
+/* src/styles.css */
+: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;
+}

Plik diff jest za duży
+ 264 - 0
tldraw/tldraw-logseq/dist/index.js


Plik diff jest za duży
+ 14127 - 0
tldraw/tldraw-logseq/dist/index.mjs


+ 38 - 0
tldraw/tldraw-logseq/dist/package.json

@@ -0,0 +1,38 @@
+{
+  "version": "0.0.0-dev",
+  "name": "tldraw-logseq",
+  "license": "MIT",
+  "main": "./index.js",
+  "module": "./index.mjs",
+  "scripts": {
+    "build": "zx build.mjs",
+    "dev": "tsup --watch",
+    "dev:vite": "tsup --watch --sourcemap inline"
+  },
+  "devDependencies": {
+    "@radix-ui/react-icons": "^1.0.3",
+    "@tldraw/core": "2.0.0-alpha.1",
+    "@tldraw/react": "2.0.0-alpha.1",
+    "@tldraw/vec": "2.0.0-alpha.1",
+    "@types/node": "^14.14.35",
+    "@types/react": "^16.9.55",
+    "@types/react-dom": "^16.9.9",
+    "concurrently": "^7.0.0",
+    "esbuild": "^0.13.8",
+    "mobx": "^6.3.7",
+    "mobx-react-lite": "^3.2.2",
+    "perfect-freehand": "^1.0.16",
+    "react": ">=16.8",
+    "react-dom": "^16.8 || ^17.0",
+    "react-select": "^5.3.2",
+    "rimraf": "3.0.2",
+    "shadow-cljs": "^2.18.0",
+    "tsup": "^5.12.7",
+    "typescript": "^4.6.0",
+    "zx": "^6.1.0"
+  },
+  "peerDependencies": {
+    "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+    "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+  }
+}

+ 38 - 0
tldraw/tldraw-logseq/package.json

@@ -0,0 +1,38 @@
+{
+  "version": "0.0.0-dev",
+  "name": "tldraw-logseq",
+  "license": "MIT",
+  "main": "dist/index.js",
+  "module": "dist/index.mjs",
+  "scripts": {
+    "build": "zx build.mjs",
+    "dev": "tsup --watch",
+    "dev:vite": "tsup --watch --sourcemap inline"
+  },
+  "devDependencies": {
+    "@radix-ui/react-icons": "^1.0.3",
+    "@tldraw/core": "2.0.0-alpha.1",
+    "@tldraw/react": "2.0.0-alpha.1",
+    "@tldraw/vec": "2.0.0-alpha.1",
+    "@types/node": "^14.14.35",
+    "@types/react": "^16.9.55",
+    "@types/react-dom": "^16.9.9",
+    "concurrently": "^7.0.0",
+    "esbuild": "^0.13.8",
+    "mobx": "^6.3.7",
+    "mobx-react-lite": "^3.2.2",
+    "perfect-freehand": "^1.0.16",
+    "react": ">=16.8",
+    "react-dom": "^16.8 || ^17.0",
+    "react-select": "^5.3.2",
+    "rimraf": "3.0.2",
+    "shadow-cljs": "^2.18.0",
+    "tsup": "^5.12.7",
+    "typescript": "^4.6.0",
+    "zx": "^6.1.0"
+  },
+  "peerDependencies": {
+    "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+    "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+  }
+}

+ 111 - 0
tldraw/tldraw-logseq/src/app.tsx

@@ -0,0 +1,111 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { TLDocumentModel } from '@tldraw/core'
+import type {
+  TLReactCallbacks,
+  TLReactComponents,
+  TLReactShapeConstructor,
+  TLReactToolConstructor,
+} from '@tldraw/react'
+import { AppCanvas, AppProvider } from '@tldraw/react'
+import * as React from 'react'
+import { AppUI } from '~components/AppUI'
+import { ContextBar } from '~components/ContextBar/ContextBar'
+import { useFileDrop } from '~hooks/useFileDrop'
+import { LogseqContext } from '~lib/logseq-context'
+import {
+  BoxShape,
+  CodeSandboxShape,
+  DotShape,
+  EllipseShape,
+  HighlighterShape,
+  ImageShape,
+  LineShape,
+  PenShape,
+  PolygonShape,
+  PolylineShape,
+  Shape,
+  StarShape,
+  TextShape,
+  YouTubeShape,
+  LogseqPortalShape,
+} from '~lib/shapes'
+import {
+  BoxTool,
+  CodeSandboxTool,
+  DotTool,
+  EllipseTool,
+  HighlighterTool,
+  LineTool,
+  LogseqPortalTool,
+  NuEraseTool,
+  PenTool,
+  PolygonTool,
+  StarTool,
+  TextTool,
+  YouTubeTool,
+} from '~lib/tools'
+
+const components: TLReactComponents<Shape> = {
+  ContextBar: ContextBar,
+}
+
+const shapes: TLReactShapeConstructor<Shape>[] = [
+  BoxShape,
+  CodeSandboxShape,
+  DotShape,
+  EllipseShape,
+  HighlighterShape,
+  ImageShape,
+  LineShape,
+  PenShape,
+  PolygonShape,
+  PolylineShape,
+  StarShape,
+  TextShape,
+  YouTubeShape,
+  LogseqPortalShape,
+]
+
+const tools: TLReactToolConstructor<Shape>[] = [
+  BoxTool,
+  CodeSandboxTool,
+  DotTool,
+  EllipseTool,
+  NuEraseTool,
+  HighlighterTool,
+  LineTool,
+  PenTool,
+  PolygonTool,
+  StarTool,
+  TextTool,
+  YouTubeTool,
+  LogseqPortalTool,
+]
+
+interface LogseqTldrawProps {
+  PageComponent: any
+  searchHandler: (query: string) => string[]
+  model?: TLDocumentModel<Shape>
+  onMount?: TLReactCallbacks<Shape>['onMount']
+  onPersist?: TLReactCallbacks<Shape>['onPersist']
+}
+
+export const App = function App(props: LogseqTldrawProps): JSX.Element {
+  const onFileDrop = useFileDrop()
+
+  const Page = React.useMemo(() => React.memo(props.PageComponent), []);
+
+  return (
+    <LogseqContext.Provider
+      value={{ Page, search: props.searchHandler }}
+    >
+      <AppProvider Shapes={shapes} Tools={tools} onFileDrop={onFileDrop} {...props}>
+        <div className="logseq-tldraw logseq-tldraw-wrapper">
+          <AppCanvas components={components} />
+          <AppUI />
+        </div>
+      </AppProvider>
+    </LogseqContext.Provider>
+  )
+}

+ 15 - 0
tldraw/tldraw-logseq/src/components/AppUI.tsx

@@ -0,0 +1,15 @@
+import * as React from 'react'
+import { observer } from 'mobx-react-lite'
+import { ToolBar } from './Toolbar'
+import { StatusBar } from './StatusBar'
+import { PrimaryTools } from './PrimaryTools'
+
+export const AppUI = observer(function AppUI() {
+  return (
+    <>
+      {/* <ToolBar /> */}
+      {/* <StatusBar /> */}
+      <PrimaryTools />
+    </>
+  )
+})

+ 9 - 0
tldraw/tldraw-logseq/src/components/Button/Button.tsx

@@ -0,0 +1,9 @@
+import * as React from 'react'
+
+export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+  children: React.ReactNode
+}
+
+export function Button(props: ButtonProps) {
+  return <button className="button" {...props} />
+}

+ 1 - 0
tldraw/tldraw-logseq/src/components/Button/index.ts

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

+ 143 - 0
tldraw/tldraw-logseq/src/components/ContextBar/ContextBar.tsx

@@ -0,0 +1,143 @@
+import * as React from 'react'
+import {
+  HTMLContainer,
+  TLContextBarComponent,
+  useApp,
+  getContextBarTranslation,
+} from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import type { TextShape, StarShape, PolygonShape, Shape } from '~lib/shapes'
+import { NumberInput } from '~components/inputs/NumberInput'
+import { ColorInput } from '~components/inputs/ColorInput'
+
+const _ContextBar: TLContextBarComponent<Shape> = ({
+  shapes,
+  offset,
+  scaledBounds,
+  // rotation,
+}) => {
+  const app = useApp()
+  const rSize = React.useRef([0, 0])
+  const rContextBar = React.useRef<HTMLDivElement>(null)
+
+  const updateStroke = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(e => {
+    shapes.forEach(shape => shape.update({ stroke: e.currentTarget.value }))
+  }, [])
+
+  const updateFill = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(e => {
+    shapes.forEach(shape => shape.update({ fill: e.currentTarget.value }))
+  }, [])
+
+  const updateStrokeWidth = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(e => {
+    shapes.forEach(shape => shape.update({ strokeWidth: +e.currentTarget.value }))
+  }, [])
+
+  const updateOpacity = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(e => {
+    shapes.forEach(shape => shape.update({ opacity: +e.currentTarget.value }))
+  }, [])
+
+  const updateSides = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(e => {
+    shapes.forEach(shape => shape.update({ sides: +e.currentTarget.value }))
+  }, [])
+
+  const updateRatio = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(e => {
+    shapes.forEach(shape => shape.update({ ratio: +e.currentTarget.value }))
+  }, [])
+
+  const updateFontSize = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(e => {
+    textShapes.forEach(shape => shape.update({ fontSize: +e.currentTarget.value }))
+  }, [])
+
+  const updateFontWeight = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(e => {
+    textShapes.forEach(shape => shape.update({ fontWeight: +e.currentTarget.value }))
+  }, [])
+
+  React.useLayoutEffect(() => {
+    const elm = rContextBar.current
+    if (!elm) return
+    const { offsetWidth, offsetHeight } = elm
+    rSize.current = [offsetWidth, offsetHeight]
+  }, [])
+
+  React.useLayoutEffect(() => {
+    const elm = rContextBar.current
+    if (!elm) return
+    const size = rSize.current
+    const [x, y] = getContextBarTranslation(size, { ...offset, bottom: offset.bottom - 32 })
+    elm.style.setProperty('transform', `translateX(${x}px) translateY(${y}px)`)
+  }, [scaledBounds, offset])
+
+  if (!app) return null
+
+  const textShapes = shapes.filter(shape => shape.type === 'text') as TextShape[]
+
+  const sidesShapes = shapes.filter(shape => 'sides' in shape.props) as (PolygonShape | StarShape)[]
+
+  const ShapeContent =
+    shapes.length === 1 && 'ReactContextBar' in shapes[0] ? shapes[0]['ReactContextBar'] : null
+
+  return (
+    <HTMLContainer centered>
+      <div ref={rContextBar} className="contextbar">
+        {ShapeContent ? (
+          <ShapeContent />
+        ) : (
+          <>
+            <ColorInput label="Stroke" value={shapes[0].props.stroke} onChange={updateStroke} />
+            <ColorInput label="Fill" value={shapes[0].props.fill} onChange={updateFill} />
+            <NumberInput
+              label="Width"
+              value={Math.max(...shapes.map(shape => shape.props.strokeWidth))}
+              onChange={updateStrokeWidth}
+              style={{ width: 48 }}
+            />
+            {sidesShapes.length > 0 && (
+              <NumberInput
+                label="Sides"
+                value={Math.max(...sidesShapes.map(shape => shape.props.sides))}
+                onChange={updateSides}
+                style={{ width: 40 }}
+              />
+            )}
+            {sidesShapes.length > 0 && (
+              <NumberInput
+                label="Ratio"
+                value={Math.max(...sidesShapes.map(shape => shape.props.ratio))}
+                onChange={updateRatio}
+                step={0.1}
+                min={0}
+                max={2}
+                style={{ width: 40 }}
+              />
+            )}
+            <NumberInput
+              label="Opacity"
+              value={Math.max(...shapes.map(shape => shape.props.opacity))}
+              onChange={updateOpacity}
+              step={0.1}
+              style={{ width: 48 }}
+            />
+            {textShapes.length > 0 ? (
+              <>
+                <NumberInput
+                  label="Size"
+                  value={Math.max(...textShapes.map(shape => shape.props.fontSize))}
+                  onChange={updateFontSize}
+                  style={{ width: 48 }}
+                />
+                <NumberInput
+                  label=" Weight"
+                  value={Math.max(...textShapes.map(shape => shape.props.fontWeight))}
+                  onChange={updateFontWeight}
+                  style={{ width: 48 }}
+                />
+              </>
+            ) : null}
+          </>
+        )}
+      </div>
+    </HTMLContainer>
+  )
+}
+
+export const ContextBar = observer(_ContextBar)

+ 1 - 0
tldraw/tldraw-logseq/src/components/ContextBar/index.ts

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

+ 145 - 0
tldraw/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx

@@ -0,0 +1,145 @@
+import * as React from 'react'
+import { useApp } from '@tldraw/react'
+import {
+  CursorArrowIcon,
+  CircleIcon,
+  Pencil1Icon,
+  VercelLogoIcon,
+  StarIcon,
+  ShadowIcon,
+  BoxIcon,
+  CodeIcon,
+  VideoIcon,
+  TextIcon,
+} from '@radix-ui/react-icons'
+import { observer } from 'mobx-react-lite'
+import { Button } from '~components/Button'
+import { EraserIcon, LineIcon } from '~components/icons'
+
+export const PrimaryTools = observer(function PrimaryTools() {
+  const app = useApp()
+
+  const handleToolClick = React.useCallback(
+    (e: React.MouseEvent<HTMLButtonElement>) => {
+      const tool = e.currentTarget.dataset.tool
+      if (tool) app.selectTool(tool)
+    },
+    [app]
+  )
+
+  const handleToolDoubleClick = React.useCallback(
+    (e: React.MouseEvent<HTMLButtonElement>) => {
+      const tool = e.currentTarget.dataset.tool
+      if (tool) app.selectTool(tool)
+      app.settings.update({ isToolLocked: true })
+    },
+    [app]
+  )
+
+  const selectedToolId = app.selectedTool.id
+
+  return (
+    <div className="primary-tools">
+      <button className="floating-button"></button>
+      <div className="panel floating-panel" data-tool-locked={app.settings.isToolLocked}>
+        <Button
+          data-tool="select"
+          data-selected={selectedToolId === 'select'}
+          onClick={handleToolClick}
+        >
+          <CursorArrowIcon />
+        </Button>
+        <Button data-tool="pen" data-selected={selectedToolId === 'pen'} onClick={handleToolClick}>
+          <Pencil1Icon />
+        </Button>
+        <Button
+          data-tool="highlighter"
+          data-selected={selectedToolId === 'highlighter'}
+          onClick={handleToolClick}
+        >
+          <ShadowIcon />
+        </Button>
+        <Button
+          data-tool="erase"
+          data-selected={selectedToolId === 'erase'}
+          onClick={handleToolClick}
+        >
+          <EraserIcon />
+        </Button>
+        <Button
+          data-tool="box"
+          data-selected={selectedToolId === 'box'}
+          onClick={handleToolClick}
+          onDoubleClick={handleToolDoubleClick}
+        >
+          <BoxIcon />
+        </Button>
+        <Button
+          data-tool="ellipse"
+          data-selected={selectedToolId === 'ellipse'}
+          onClick={handleToolClick}
+          onDoubleClick={handleToolDoubleClick}
+        >
+          <CircleIcon />
+        </Button>
+        <Button
+          data-tool="polygon"
+          data-selected={selectedToolId === 'polygon'}
+          onClick={handleToolClick}
+          onDoubleClick={handleToolDoubleClick}
+        >
+          <VercelLogoIcon />
+        </Button>
+        <Button
+          data-tool="star"
+          data-selected={selectedToolId === 'star'}
+          onClick={handleToolClick}
+          onDoubleClick={handleToolDoubleClick}
+        >
+          <StarIcon />
+        </Button>
+        <Button
+          data-tool="line"
+          data-selected={selectedToolId === 'line'}
+          onClick={handleToolClick}
+          onDoubleClick={handleToolDoubleClick}
+        >
+          <LineIcon />
+        </Button>
+        <Button
+          data-tool="text"
+          data-selected={selectedToolId === 'text'}
+          onClick={handleToolClick}
+          onDoubleClick={handleToolDoubleClick}
+        >
+          <TextIcon />
+        </Button>
+        <Button
+          data-tool="code"
+          data-selected={selectedToolId === 'code'}
+          onClick={handleToolClick}
+          onDoubleClick={handleToolDoubleClick}
+        >
+          <CodeIcon />
+        </Button>
+        <Button
+          data-tool="youtube"
+          data-selected={selectedToolId === 'youtube'}
+          onClick={handleToolClick}
+          onDoubleClick={handleToolDoubleClick}
+        >
+          <VideoIcon />
+        </Button>
+        <Button
+          data-tool="logseq-portal"
+          data-selected={selectedToolId === 'logseq-portal'}
+          onClick={handleToolClick}
+          onDoubleClick={handleToolDoubleClick}
+        >
+          🥹
+        </Button>
+      </div>
+      <button className="floating-button"></button>
+    </div>
+  )
+})

+ 1 - 0
tldraw/tldraw-logseq/src/components/PrimaryTools/index.ts

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

+ 15 - 0
tldraw/tldraw-logseq/src/components/StatusBar/StatusBar.tsx

@@ -0,0 +1,15 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { observer } from 'mobx-react-lite'
+import { useApp } from '@tldraw/react'
+import type { Shape } from '~lib'
+
+export const StatusBar = observer(function StatusBar() {
+  const app = useApp<Shape>()
+  return (
+    <div className="statusbar">
+      {app.selectedTool.id} | {app.selectedTool.currentState.id}
+    </div>
+  )
+})

+ 1 - 0
tldraw/tldraw-logseq/src/components/StatusBar/index.ts

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

+ 67 - 0
tldraw/tldraw-logseq/src/components/Toolbar/ToolBar.tsx

@@ -0,0 +1,67 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { observer } from 'mobx-react-lite'
+import type { Shape } from '~lib'
+import { useApp } from '@tldraw/react'
+
+export const ToolBar = observer(function ToolBar(): JSX.Element {
+  const app = useApp<Shape>()
+
+  const zoomIn = React.useCallback(() => {
+    app.api.zoomIn()
+  }, [app])
+
+  const zoomOut = React.useCallback(() => {
+    app.api.zoomOut()
+  }, [app])
+
+  const resetZoom = React.useCallback(() => {
+    app.api.resetZoom()
+  }, [app])
+
+  const zoomToFit = React.useCallback(() => {
+    app.api.zoomToFit()
+  }, [app])
+
+  const zoomToSelection = React.useCallback(() => {
+    app.api.zoomToSelection()
+  }, [app])
+
+  const sendToBack = React.useCallback(() => {
+    app.sendToBack()
+  }, [app])
+
+  const sendBackward = React.useCallback(() => {
+    app.sendBackward()
+  }, [app])
+
+  const bringToFront = React.useCallback(() => {
+    app.bringToFront()
+  }, [app])
+
+  const bringForward = React.useCallback(() => {
+    app.bringForward()
+  }, [app])
+
+  const flipHorizontal = React.useCallback(() => {
+    app.flipHorizontal()
+  }, [app])
+
+  const flipVertical = React.useCallback(() => {
+    app.flipVertical()
+  }, [app])
+
+  return (
+    <div className="toolbar">
+      <button onClick={sendToBack}>Send to Back</button>
+      <button onClick={sendBackward}>Send Backward</button>
+      <button onClick={bringForward}>Bring Forward</button>
+      <button onClick={bringToFront}>Bring To Front</button>|<button onClick={zoomOut}>-</button>
+      <button onClick={zoomIn}>+</button>
+      <button onClick={resetZoom}>reset</button>
+      <button onClick={zoomToFit}>zoom to fit</button>
+      <button onClick={zoomToSelection}>zoom to selection</button>
+    </div>
+  )
+})

+ 1 - 0
tldraw/tldraw-logseq/src/components/Toolbar/index.ts

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

+ 25 - 0
tldraw/tldraw-logseq/src/components/icons/BoxIcon.tsx

@@ -0,0 +1,25 @@
+import * as React from 'react'
+
+export function BoxIcon({
+  fill = 'none',
+  stroke = 'currentColor',
+  strokeWidth = 2,
+}: {
+  fill?: string
+  stroke?: string
+  strokeWidth?: number
+}): JSX.Element {
+  return (
+    <svg
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      stroke={stroke}
+      strokeWidth={strokeWidth}
+      fill={fill}
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <rect x="4" y="4" width="16" height="16" rx="2" />
+    </svg>
+  )
+}

+ 14 - 0
tldraw/tldraw-logseq/src/components/icons/CircleIcon.tsx

@@ -0,0 +1,14 @@
+import * as React from 'react'
+
+export function CircleIcon(
+  props: Pick<React.SVGProps<SVGSVGElement>, 'strokeWidth' | 'stroke' | 'fill'> & {
+    size?: number
+  }
+) {
+  const { size = 16, ...rest } = props
+  return (
+    <svg width={24} height={24} {...rest}>
+      <circle cx={12} cy={12} r={size / 2} fill="none" stroke="currentColor" />
+    </svg>
+  )
+}

+ 17 - 0
tldraw/tldraw-logseq/src/components/icons/DashDashedIcon.tsx

@@ -0,0 +1,17 @@
+import * as React from 'react'
+
+export function DashDashedIcon(): JSX.Element {
+  return (
+    <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
+      <circle
+        cx={12}
+        cy={12}
+        r={8}
+        fill="none"
+        strokeWidth={2.5}
+        strokeLinecap="round"
+        strokeDasharray={50.26548 * 0.1}
+      />
+    </svg>
+  )
+}

+ 19 - 0
tldraw/tldraw-logseq/src/components/icons/DashDottedIcon.tsx

@@ -0,0 +1,19 @@
+import * as React from 'react'
+
+const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
+
+export function DashDottedIcon(): JSX.Element {
+  return (
+    <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
+      <circle
+        cx={12}
+        cy={12}
+        r={8}
+        fill="none"
+        strokeWidth={2.5}
+        strokeLinecap="round"
+        strokeDasharray={dottedDasharray}
+      />
+    </svg>
+  )
+}

+ 19 - 0
tldraw/tldraw-logseq/src/components/icons/DashDrawIcon.tsx

@@ -0,0 +1,19 @@
+import * as React from 'react'
+
+export function DashDrawIcon(): JSX.Element {
+  return (
+    <svg
+      width="24"
+      height="24"
+      viewBox="1 1.5 21 22"
+      fill="currentColor"
+      stroke="currentColor"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path
+        d="M10.0162 19.2768C10.0162 19.2768 9.90679 19.2517 9.6879 19.2017C9.46275 19.1454 9.12816 19.0422 8.68413 18.8921C8.23384 18.7358 7.81482 18.545 7.42707 18.3199C7.03307 18.101 6.62343 17.7883 6.19816 17.3818C5.77289 16.9753 5.33511 16.3718 4.88482 15.5713C4.43453 14.7645 4.1531 13.8545 4.04053 12.8414C3.92795 11.822 4.04991 10.8464 4.40639 9.91451C4.76286 8.98266 5.39452 8.10084 6.30135 7.26906C7.21444 6.44353 8.29325 5.83377 9.5378 5.43976C10.7823 5.05202 11.833 4.92068 12.6898 5.04576C13.5466 5.16459 14.3878 5.43664 15.2133 5.86191C16.0388 6.28718 16.7768 6.8688 17.4272 7.60678C18.0714 8.34475 18.5404 9.21406 18.8344 10.2147C19.1283 11.2153 19.1721 12.2598 18.9657 13.348C18.7593 14.4299 18.2872 15.4337 17.5492 16.3593C16.8112 17.2849 15.9263 18.0072 14.8944 18.5263C13.8624 19.0391 12.9056 19.3174 12.0238 19.3612C11.142 19.405 10.2101 19.2705 9.22823 18.9578C8.24635 18.6451 7.35828 18.151 6.56402 17.4756C5.77601 16.8002 6.08871 16.8658 7.50212 17.6726C8.90927 18.4731 10.1444 18.8484 11.2076 18.7983C12.2645 18.7545 13.2965 18.4825 14.3034 17.9822C15.3102 17.4819 16.1264 16.8221 16.7518 16.0028C17.3772 15.1835 17.7681 14.3111 17.9244 13.3855C18.0808 12.4599 18.0401 11.5781 17.8025 10.74C17.5586 9.902 17.1739 9.15464 16.6486 8.49797C16.1233 7.8413 15.2289 7.27844 13.9656 6.80939C12.7086 6.34034 11.4203 6.20901 10.1007 6.41539C8.78732 6.61552 7.69599 7.06893 6.82669 7.77564C5.96363 8.48859 5.34761 9.26409 4.97863 10.1021C4.60964 10.9402 4.45329 11.8376 4.50958 12.7945C4.56586 13.7513 4.79101 14.6238 5.18501 15.4118C5.57276 16.1998 5.96363 16.8002 6.35764 17.2129C6.75164 17.6257 7.13313 17.9509 7.50212 18.1886C7.87736 18.4325 8.28074 18.642 8.71227 18.8171C9.15005 18.9922 9.47839 19.111 9.69728 19.1736C9.91617 19.2361 10.0256 19.2705 10.0256 19.2768H10.0162Z"
+        strokeWidth="2"
+      />
+    </svg>
+  )
+}

+ 9 - 0
tldraw/tldraw-logseq/src/components/icons/DashSolidIcon.tsx

@@ -0,0 +1,9 @@
+import * as React from 'react'
+
+export function DashSolidIcon(): JSX.Element {
+  return (
+    <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
+      <circle cx={12} cy={12} r={8} fill="none" strokeWidth={2} strokeLinecap="round" />
+    </svg>
+  )
+}

+ 15 - 0
tldraw/tldraw-logseq/src/components/icons/DiscordIcon.tsx

@@ -0,0 +1,15 @@
+import * as React from 'react'
+
+export function DiscordIcon() {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="16"
+      height="16"
+      fill="currentColor"
+      viewBox="0 0 16 16"
+    >
+      <path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z" />
+    </svg>
+  )
+}

+ 21 - 0
tldraw/tldraw-logseq/src/components/icons/EraserIcon.tsx

@@ -0,0 +1,21 @@
+import * as React from 'react'
+
+export function EraserIcon(): JSX.Element {
+  return (
+    <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+      <path
+        d="M1.72838 9.33987L8.84935 2.34732C9.23874 1.96494 9.86279 1.96539 10.2516 2.34831L13.5636 5.60975C13.9655 6.00555 13.9607 6.65526 13.553 7.04507L8.13212 12.2278C7.94604 12.4057 7.69851 12.505 7.44107 12.505L6.06722 12.505L3.83772 12.505C3.5673 12.505 3.30842 12.3954 3.12009 12.2014L1.7114 10.7498C1.32837 10.3551 1.33596 9.72521 1.72838 9.33987Z"
+        stroke="currentColor"
+      />
+      <line
+        x1="6.01807"
+        y1="12.5"
+        x2="10.7959"
+        y2="12.5"
+        stroke="currentColor"
+        strokeLinecap="round"
+      />
+      <line x1="5.50834" y1="5.74606" x2="10.1984" y2="10.4361" stroke="currentColor" />
+    </svg>
+  )
+}

+ 14 - 0
tldraw/tldraw-logseq/src/components/icons/HeartIcon.tsx

@@ -0,0 +1,14 @@
+import * as React from 'react'
+
+export function HeartIcon() {
+  return (
+    <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+      <path
+        fill="none"
+        stroke="currentColor"
+        strokeWidth={2}
+        d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
+      />
+    </svg>
+  )
+}

+ 18 - 0
tldraw/tldraw-logseq/src/components/icons/IsFilledIcon.tsx

@@ -0,0 +1,18 @@
+import * as React from 'react'
+
+export function IsFilledIcon(): JSX.Element {
+  return (
+    <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+      <rect
+        x="4"
+        y="4"
+        width="16"
+        height="16"
+        rx="2"
+        strokeWidth="2"
+        fill="currentColor"
+        opacity=".9"
+      />
+    </svg>
+  )
+}

+ 15 - 0
tldraw/tldraw-logseq/src/components/icons/LineIcon.tsx

@@ -0,0 +1,15 @@
+import * as React from 'react'
+
+export function LineIcon() {
+  return (
+    <svg
+      width="15"
+      height="15"
+      viewBox="0 0 15 15"
+      fill="currentColor"
+      xmlns="http://www.w3.org/2000/svg"
+    >
+      <path d="M3.64645 11.3536C3.45118 11.1583 3.45118 10.8417 3.64645 10.6465L11.1464 3.14645C11.3417 2.95118 11.6583 2.95118 11.8536 3.14645C12.0488 3.34171 12.0488 3.65829 11.8536 3.85355L4.35355 11.3536C4.15829 11.5488 3.84171 11.5488 3.64645 11.3536Z" />
+    </svg>
+  )
+}

+ 14 - 0
tldraw/tldraw-logseq/src/components/icons/MultiplayerIcon.tsx

@@ -0,0 +1,14 @@
+import * as React from 'react'
+
+export function MultiplayerIcon(): JSX.Element {
+  return (
+    <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M6.20634 0.691501C6.38439 0.610179 6.59352 0.640235 6.74146 0.768408L14.4999 7.49004C14.6546 7.62409 14.712 7.83893 14.6447 8.03228C14.5774 8.22564 14.399 8.35843 14.1945 8.36745L11.4343 8.48919L13.0504 12.04C13.1647 12.2913 13.0538 12.5877 12.8026 12.7022L10.9536 13.5444C10.7023 13.6589 10.4059 13.5481 10.2913 13.2968L8.67272 9.74659L6.83997 11.679L7.40596 12.9226C7.52032 13.1739 7.40939 13.4703 7.15815 13.5848L5.56925 14.3086C5.31801 14.423 5.02156 14.3122 4.90703 14.061L3.55936 11.105L2.00195 12.7471C1.86112 12.8956 1.64403 12.9433 1.45395 12.8675C1.26386 12.7917 1.13916 12.6077 1.13916 12.4031V3.59046C1.13916 3.39472 1.25338 3.21698 1.43143 3.13565C1.60949 3.05433 1.81862 3.08439 1.96656 3.21256L5.91406 6.63254V1.14631C5.91406 0.950565 6.02829 0.772823 6.20634 0.691501ZM7.13109 9.91888L6.91406 10.1477V9.92845V8.92747V8.82201V7.49891V2.24104L12.8962 7.42374L10.6504 7.52279C10.4845 7.53011 10.333 7.61939 10.2462 7.76104C10.2353 7.77874 10.2256 7.79698 10.2172 7.81563C10.1579 7.94618 10.1572 8.0971 10.2174 8.2294C10.2174 8.22941 10.2174 8.22941 10.2174 8.22942L11.9332 11.9993L10.9939 12.4272L9.27506 8.65717C9.2061 8.50591 9.06648 8.39882 8.90252 8.37142C8.73857 8.34402 8.57171 8.3999 8.45732 8.52051L8.2931 8.69366L7.1311 9.91887L7.13109 9.91888ZM5.91406 8.97158V7.95564L2.13916 4.68519V11.1493L3.34396 9.87894C3.45835 9.75833 3.62521 9.70245 3.78916 9.72985C3.95312 9.75725 4.09274 9.86434 4.1617 10.0156L5.60957 13.1913L6.28874 12.8819L4.84345 9.70633C4.77463 9.55512 4.78541 9.3796 4.87222 9.23795C4.95903 9.0963 5.11053 9.00702 5.2765 8.9997L5.91406 8.97158Z"
+        fill="black"
+      />
+    </svg>
+  )
+}

+ 16 - 0
tldraw/tldraw-logseq/src/components/icons/RedoIcon.tsx

@@ -0,0 +1,16 @@
+import * as React from 'react'
+
+export function RedoIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
+  return (
+    <svg
+      width={32}
+      height={32}
+      viewBox="0 0 15 15"
+      fill="currentColor"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path d="M4.32978 8.5081C4.32978 10.1923 5.70009 11.5625 7.38418 11.5625H8.46539C8.64456 11.5625 8.78975 11.4173 8.78975 11.2382V11.13C8.78975 10.9508 8.64457 10.8057 8.46539 10.8057H7.38418C6.11736 10.8057 5.08662 9.77492 5.08662 8.5081C5.08662 7.24128 6.11736 6.21054 7.38418 6.21054H9.37894L8.00515 7.58433C7.8576 7.73183 7.8576 7.97195 8.00515 8.11944C8.14833 8.26251 8.39751 8.2623 8.54036 8.11944L10.56 6.09971C10.6315 6.02824 10.6709 5.93321 10.6709 5.8321C10.6709 5.73106 10.6315 5.63598 10.56 5.56454L8.54025 3.54472C8.3974 3.40176 8.14801 3.40176 8.00513 3.54472C7.85758 3.69218 7.85758 3.93234 8.00513 4.07979L9.37892 5.45368H7.38418C5.70009 5.45368 4.32978 6.82393 4.32978 8.5081Z" />
+    </svg>
+  )
+}

+ 16 - 0
tldraw/tldraw-logseq/src/components/icons/SizeLargeIcon.tsx

@@ -0,0 +1,16 @@
+import * as React from 'react'
+
+export function SizeLargeIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
+  return (
+    <svg
+      width={24}
+      height={24}
+      viewBox="-2 -2 28 28"
+      fill="currentColor"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path d="M7.68191 19C7.53525 19 7.46191 18.9267 7.46191 18.78V5H10.1219C10.2686 5 10.3419 5.07333 10.3419 5.22V16.56H13.4419V15.02H15.7619C15.9086 15.02 15.9819 15.0933 15.9819 15.24V19H7.68191Z" />
+    </svg>
+  )
+}

+ 16 - 0
tldraw/tldraw-logseq/src/components/icons/SizeMediumIcon.tsx

@@ -0,0 +1,16 @@
+import * as React from 'react'
+
+export function SizeMediumIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
+  return (
+    <svg
+      width={24}
+      height={24}
+      viewBox="-2 -2 28 28"
+      fill="currentColor"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path d="M8.16191 19H5.68191C5.53525 19 5.46191 18.9267 5.46191 18.78V5H8.76191C8.88191 5 8.97525 5.03333 9.04191 5.1C9.10858 5.15333 9.17525 5.27333 9.24191 5.46C9.72191 6.59333 10.1686 7.7 10.5819 8.78C11.0086 9.84667 11.4352 10.98 11.8619 12.18H12.1619C12.6019 10.9667 13.0352 9.79333 13.4619 8.66C13.8886 7.52667 14.3552 6.30667 14.8619 5H18.3219C18.4686 5 18.5419 5.07333 18.5419 5.22V19H16.0619C15.9152 19 15.8419 18.9267 15.8419 18.78V16.26C15.8419 15.5267 15.8486 14.8133 15.8619 14.12C15.8886 13.4267 15.9286 12.6867 15.9819 11.9C16.0486 11.1 16.1419 10.1933 16.2619 9.18H15.9019C15.4352 10.3533 14.9486 11.5667 14.4419 12.82C13.9486 14.06 13.4819 15.2333 13.0419 16.34H11.1019C11.0619 16.34 11.0152 16.3333 10.9619 16.32C10.9219 16.2933 10.8886 16.2467 10.8619 16.18C10.4619 15.18 10.0086 14.06 9.50191 12.82C9.00858 11.58 8.53525 10.3667 8.08191 9.18H7.70191C7.83525 10.18 7.93525 11.0733 8.00191 11.86C8.06858 12.6467 8.10858 13.3933 8.12191 14.1C8.14858 14.8067 8.16191 15.5267 8.16191 16.26V19Z" />
+    </svg>
+  )
+}

+ 16 - 0
tldraw/tldraw-logseq/src/components/icons/SizeSmallIcon.tsx

@@ -0,0 +1,16 @@
+import * as React from 'react'
+
+export function SizeSmallIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
+  return (
+    <svg
+      width={24}
+      height={24}
+      viewBox="-2 -2 28 28"
+      fill="currentColor"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path d="M12.4239 4.62C13.3572 4.62 14.1572 4.73333 14.8239 4.96C15.4906 5.17333 15.9772 5.43333 16.2839 5.74C16.3639 5.82 16.4039 5.94 16.4039 6.1V8.86H14.0639C13.9172 8.86 13.8439 8.78666 13.8439 8.64V7.26C13.4306 7.12666 12.9572 7.06 12.4239 7.06C11.6506 7.06 11.0639 7.18 10.6639 7.42C10.2639 7.66 10.0639 8.04666 10.0639 8.58V9C10.0639 9.38666 10.1639 9.69333 10.3639 9.92C10.5772 10.1333 11.0306 10.3467 11.7239 10.56L13.6439 11.14C14.4706 11.38 15.1172 11.66 15.5839 11.98C16.0506 12.3 16.3772 12.68 16.5639 13.12C16.7639 13.5467 16.8639 14.0733 16.8639 14.7V15.62C16.8639 16.7933 16.4039 17.7133 15.4839 18.38C14.5639 19.0467 13.2839 19.38 11.6439 19.38C10.6706 19.38 9.79723 19.2867 9.0239 19.1C8.2639 18.9133 7.71056 18.6533 7.3639 18.32C7.3239 18.28 7.29056 18.24 7.2639 18.2C7.25056 18.1467 7.2439 18.06 7.2439 17.94V15.74H7.6239C8.2239 16.1533 8.85056 16.4533 9.5039 16.64C10.1572 16.8267 10.9306 16.92 11.8239 16.92C12.6506 16.92 13.2506 16.7867 13.6239 16.52C14.0106 16.2533 14.2039 15.9333 14.2039 15.56V14.88C14.2039 14.6667 14.1639 14.48 14.0839 14.32C14.0172 14.16 13.8706 14.0133 13.6439 13.88C13.4172 13.7467 13.0572 13.6067 12.5639 13.46L10.6639 12.88C9.7839 12.6133 9.11056 12.3 8.6439 11.94C8.17723 11.58 7.85056 11.18 7.6639 10.74C7.49056 10.3 7.4039 9.83333 7.4039 9.34V8.38C7.4039 7.64666 7.61056 7 8.0239 6.44C8.43723 5.88 9.01723 5.44 9.7639 5.12C10.5239 4.78666 11.4106 4.62 12.4239 4.62Z" />
+    </svg>
+  )
+}

+ 30 - 0
tldraw/tldraw-logseq/src/components/icons/TrashIcon.tsx

@@ -0,0 +1,30 @@
+import * as React from 'react'
+
+export function TrashIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
+  return (
+    <svg
+      width={18}
+      height={18}
+      viewBox="0 0 15 15"
+      fill="currentColor"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M2 4.656a.5.5 0 01.5-.5h9.7a.5.5 0 010 1H2.5a.5.5 0 01-.5-.5z"
+      />
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M6.272 3a.578.578 0 00-.578.578v.578h3.311v-.578A.578.578 0 008.428 3H6.272zm3.733 1.156v-.578A1.578 1.578 0 008.428 2H6.272a1.578 1.578 0 00-1.578 1.578v.578H3.578a.5.5 0 00-.5.5V12.2a1.578 1.578 0 001.577 1.578h5.39a1.578 1.578 0 001.577-1.578V4.656a.5.5 0 00-.5-.5h-1.117zm-5.927 1V12.2a.578.578 0 00.577.578h5.39a.578.578 0 00.577-.578V5.156H4.078z"
+      />
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M6.272 6.85a.5.5 0 01.5.5v3.233a.5.5 0 11-1 0V7.35a.5.5 0 01.5-.5zM8.428 6.85a.5.5 0 01.5.5v3.233a.5.5 0 11-1 0V7.35a.5.5 0 01.5-.5z"
+      />
+    </svg>
+  )
+}

+ 16 - 0
tldraw/tldraw-logseq/src/components/icons/UndoIcon.tsx

@@ -0,0 +1,16 @@
+import * as React from 'react'
+
+export function UndoIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
+  return (
+    <svg
+      width={32}
+      height={32}
+      viewBox="0 0 15 15"
+      fill="currentColor"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path d="M10.6707 8.5081C10.6707 10.1923 9.3004 11.5625 7.61631 11.5625H6.5351C6.35593 11.5625 6.21074 11.4173 6.21074 11.2382V11.13C6.21074 10.9508 6.35591 10.8057 6.5351 10.8057H7.61631C8.88313 10.8057 9.91387 9.77492 9.91387 8.5081C9.91387 7.24128 8.88313 6.21054 7.61631 6.21054H5.62155L6.99534 7.58433C7.14289 7.73183 7.14289 7.97195 6.99534 8.11944C6.85216 8.26251 6.60298 8.2623 6.46013 8.11944L4.44045 6.09971C4.36898 6.02824 4.32959 5.93321 4.32959 5.8321C4.32959 5.73106 4.36898 5.63598 4.44045 5.56454L6.46024 3.54472C6.60309 3.40176 6.85248 3.40176 6.99535 3.54472C7.14291 3.69218 7.14291 3.93234 6.99535 4.07979L5.62156 5.45368H7.61631C9.3004 5.45368 10.6707 6.82393 10.6707 8.5081Z" />
+    </svg>
+  )
+}

+ 17 - 0
tldraw/tldraw-logseq/src/components/icons/index.ts

@@ -0,0 +1,17 @@
+export * from './BoxIcon'
+export * from './CircleIcon'
+export * from './DashDashedIcon'
+export * from './DashDottedIcon'
+export * from './DashDrawIcon'
+export * from './DashSolidIcon'
+export * from './IsFilledIcon'
+export * from './RedoIcon'
+export * from './TrashIcon'
+export * from './UndoIcon'
+export * from './SizeSmallIcon'
+export * from './SizeMediumIcon'
+export * from './SizeLargeIcon'
+export * from './EraserIcon'
+export * from './MultiplayerIcon'
+export * from './DiscordIcon'
+export * from './LineIcon'

+ 14 - 0
tldraw/tldraw-logseq/src/components/inputs/ColorInput.tsx

@@ -0,0 +1,14 @@
+import * as React from 'react'
+
+interface ColorInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
+  label: string
+}
+
+export function ColorInput({ label, ...rest }: ColorInputProps) {
+  return (
+    <div className="input">
+      <label htmlFor={`color-${label}`}>{label}</label>
+      <input className="color-input" name={`color-${label}`} type="color" {...rest} />
+    </div>
+  )
+}

+ 14 - 0
tldraw/tldraw-logseq/src/components/inputs/NumberInput.tsx

@@ -0,0 +1,14 @@
+import * as React from 'react'
+
+interface NumberInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
+  label: string
+}
+
+export function NumberInput({ label, ...rest }: NumberInputProps) {
+  return (
+    <div className="input">
+      <label htmlFor={`number-${label}`}>{label}</label>
+      <input className="number-input" name={`number-${label}`} type="number" {...rest} />
+    </div>
+  )
+}

+ 16 - 0
tldraw/tldraw-logseq/src/components/inputs/TextInput.tsx

@@ -0,0 +1,16 @@
+import * as React from 'react'
+
+interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
+  label: string
+}
+
+export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
+  ({ label, ...rest }, ref) => {
+    return (
+      <div className="input">
+        <label htmlFor={`text-${label}`}>{label}</label>
+        <input ref={ref} className="text-input" name={`text-${label}`} type="text" {...rest} />
+      </div>
+    )
+  }
+)

+ 35 - 0
tldraw/tldraw-logseq/src/documents/dev.ts

@@ -0,0 +1,35 @@
+import type { TLDocumentModel } from '@tldraw/core'
+import type { Shape } from '~lib'
+
+const documentModel: TLDocumentModel<Shape, any> = {
+  currentPageId: 'page1',
+  selectedIds: ['yt1', 'yt2'],
+  pages: [
+    {
+      name: 'Page',
+      id: 'page1',
+      shapes: [
+        {
+          id: 'yt1',
+          type: 'youtube',
+          parentId: 'page1',
+          point: [100, 100],
+          size: [160, 90],
+          embedId: '',
+        },
+        {
+          id: 'yt2',
+          type: 'youtube',
+          parentId: 'page1',
+          point: [300, 300],
+          size: [160, 90],
+          embedId: '',
+        },
+      ],
+      bindings: [],
+    },
+  ],
+  assets: [],
+}
+
+export default documentModel

+ 18 - 0
tldraw/tldraw-logseq/src/documents/empty.ts

@@ -0,0 +1,18 @@
+import type { TLDocumentModel } from '@tldraw/core'
+import type { Shape } from '~lib'
+
+const documentModel: TLDocumentModel<Shape, any> = {
+  currentPageId: 'page1',
+  selectedIds: [],
+  pages: [
+    {
+      name: 'Page',
+      id: 'page1',
+      shapes: [],
+      bindings: [],
+    },
+  ],
+  assets: [],
+}
+
+export default documentModel

Plik diff jest za duży
+ 27 - 0
tldraw/tldraw-logseq/src/documents/withAsset.ts


Plik diff jest za duży
+ 149 - 0
tldraw/tldraw-logseq/src/documents/withEverything.ts


+ 55 - 0
tldraw/tldraw-logseq/src/hooks/useFileDrop.ts

@@ -0,0 +1,55 @@
+import { fileToBase64, getSizeFromSrc, TLAsset, uniqueId } from '@tldraw/core'
+import type { TLReactCallbacks } from '@tldraw/react'
+import * as React from 'react'
+import type { Shape } from '~lib'
+
+export function useFileDrop() {
+  return React.useCallback<TLReactCallbacks<Shape>['onFileDrop']>(async (app, { files, point }) => {
+    const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
+    const assetId = uniqueId()
+    interface ImageAsset extends TLAsset {
+      size: number[]
+    }
+    const assetsToCreate: ImageAsset[] = []
+    for (const file of files) {
+      try {
+        // Get extension, verify that it's an image
+        const extensionMatch = file.name.match(/\.[0-9a-z]+$/i)
+        if (!extensionMatch) throw Error('No extension.')
+        const extension = extensionMatch[0].toLowerCase()
+        if (!IMAGE_EXTENSIONS.includes(extension)) continue
+        // Turn the image into a base64 dataurl
+        const dataurl = await fileToBase64(file)
+        if (typeof dataurl !== 'string') continue
+        // Do we already have an asset for this image?
+        const existingAsset = Object.values(app.assets).find(asset => asset.src === dataurl)
+        if (existingAsset) {
+          assetsToCreate.push(existingAsset as ImageAsset)
+          continue
+        }
+        // Create a new asset for this image
+        const asset: ImageAsset = {
+          id: assetId,
+          type: 'image',
+          src: dataurl,
+          size: await getSizeFromSrc(dataurl),
+        }
+        assetsToCreate.push(asset)
+      } catch (error) {
+        console.error(error)
+      }
+    }
+    app.createAssets(assetsToCreate)
+    app.createShapes(
+      assetsToCreate.map((asset, i) => ({
+        id: uniqueId(),
+        type: 'image',
+        parentId: app.currentPageId,
+        point: [point[0] - asset.size[0] / 2 + i * 16, point[1] - asset.size[1] / 2 + i * 16],
+        size: asset.size,
+        assetId: asset.id,
+        opacity: 1,
+      }))
+    )
+  }, [])
+}

+ 10 - 0
tldraw/tldraw-logseq/src/index.ts

@@ -0,0 +1,10 @@
+// export * as shapes from '~lib/shapes'
+// export * as tools from '~lib/tools'
+// export { AppUI } from '~components/AppUI'
+// export { ContextBar } from '~components/ContextBar/ContextBar'
+// export { AppCanvas, AppProvider } from '@tldraw/react'
+// export { useFileDrop } from '~hooks/useFileDrop'
+
+import './styles.css';
+
+export * from './app'

+ 3 - 0
tldraw/tldraw-logseq/src/lib/index.ts

@@ -0,0 +1,3 @@
+export * from './shapes'
+export * from './tools'
+export * from './unused-app'

+ 7 - 0
tldraw/tldraw-logseq/src/lib/logseq-context.ts

@@ -0,0 +1,7 @@
+import React from 'react'
+export const LogseqContext = React.createContext<
+  Partial<{
+    Page: React.FC<{ pageId: string }>
+    search: (query: string) => string[]
+  }>
+>({})

+ 85 - 0
tldraw/tldraw-logseq/src/lib/shapes/BoxShape.tsx

@@ -0,0 +1,85 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+export interface BoxShapeProps extends TLBoxShapeProps, CustomStyleProps {
+  borderRadius: number
+  type: 'box'
+}
+
+export class BoxShape extends TLBoxShape<BoxShapeProps> {
+  static id = 'box'
+
+  static defaultProps: BoxShapeProps = {
+    id: 'box',
+    parentId: 'page',
+    type: 'box',
+    point: [0, 0],
+    size: [100, 100],
+    borderRadius: 0,
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  ReactComponent = observer(({ events, isErasing, isSelected }: TLComponentProps) => {
+    const {
+      props: {
+        size: [w, h],
+        stroke,
+        fill,
+        strokeWidth,
+        borderRadius,
+        opacity,
+      },
+    } = this
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <rect
+          className={isSelected ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
+          x={strokeWidth / 2}
+          y={strokeWidth / 2}
+          rx={borderRadius}
+          ry={borderRadius}
+          width={Math.max(0.01, w - strokeWidth)}
+          height={Math.max(0.01, h - strokeWidth)}
+          pointerEvents="all"
+        />
+        <rect
+          x={strokeWidth / 2}
+          y={strokeWidth / 2}
+          rx={borderRadius}
+          ry={borderRadius}
+          width={Math.max(0.01, w - strokeWidth)}
+          height={Math.max(0.01, h - strokeWidth)}
+          strokeWidth={strokeWidth}
+          stroke={stroke}
+          fill={fill}
+        />
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      props: {
+        size: [w, h],
+        borderRadius,
+      },
+    } = this
+    return <rect width={w} height={h} rx={borderRadius} ry={borderRadius} fill="transparent" />
+  })
+
+  validateProps = (props: Partial<BoxShapeProps>) => {
+    if (props.size !== undefined) {
+      props.size[0] = Math.max(props.size[0], 1)
+      props.size[1] = Math.max(props.size[1], 1)
+    }
+    if (props.borderRadius !== undefined) props.borderRadius = Math.max(0, props.borderRadius)
+    return withClampedStyles(props)
+  }
+}

+ 122 - 0
tldraw/tldraw-logseq/src/lib/shapes/CodeSandboxShape.tsx

@@ -0,0 +1,122 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import type { TLBoxShapeProps } from '@tldraw/core'
+import { HTMLContainer, TLComponentProps, TLReactBoxShape } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+import { TextInput } from '~components/inputs/TextInput'
+
+export interface CodeSandboxShapeProps extends TLBoxShapeProps, CustomStyleProps {
+  type: 'code'
+  embedId: string
+}
+
+export class CodeSandboxShape extends TLReactBoxShape<CodeSandboxShapeProps> {
+  static id = 'code'
+
+  static defaultProps: CodeSandboxShapeProps = {
+    id: 'code',
+    type: 'code',
+    parentId: 'page',
+    point: [0, 0],
+    size: [600, 320],
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+    embedId: '',
+  }
+
+  canEdit = true
+
+  canFlip = false
+
+  ReactContextBar = observer(() => {
+    const { embedId } = this.props
+    const rInput = React.useRef<HTMLInputElement>(null)
+    const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+      const url = e.currentTarget.value
+      const match = url.match(/\/s\/([^?]+)/)
+      const embedId = match?.[1] ?? url ?? ''
+      this.update({ embedId })
+    }, [])
+    return (
+      <>
+        <TextInput
+          ref={rInput}
+          label="CodeSandbox Embed ID"
+          type="text"
+          value={embedId}
+          onChange={handleChange}
+        />
+      </>
+    )
+  })
+
+  ReactComponent = observer(({ events, isEditing, isErasing }: TLComponentProps) => {
+    const { opacity, embedId } = this.props
+    return (
+      <HTMLContainer
+        style={{
+          overflow: 'hidden',
+          pointerEvents: 'all',
+          opacity: isErasing ? 0.2 : opacity,
+        }}
+        {...events}
+      >
+        <div
+          style={{
+            width: '100%',
+            height: '100%',
+            pointerEvents: isEditing ? 'all' : 'none',
+            userSelect: 'none',
+          }}
+        >
+          {embedId ? (
+            <iframe
+              src={`https://codesandbox.io/embed/${embedId}?&fontsize=14&hidenavigation=1&theme=dark`}
+              style={{ width: '100%', height: '100%', overflow: 'hidden' }}
+              title={'CodeSandbox'}
+              allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
+              sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
+            />
+          ) : (
+            <div
+              style={{
+                width: '100%',
+                height: '100%',
+                display: 'flex',
+                alignItems: 'center',
+                overflow: 'hidden',
+                justifyContent: 'center',
+                backgroundColor: '#FFFFFF',
+                border: '1px solid rgb(52, 52, 52)',
+                padding: 16,
+              }}
+            >
+              <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="128">
+                <title />
+                <path d="M2 6l10.455-6L22.91 6 23 17.95 12.455 24 2 18V6zm2.088 2.481v4.757l3.345 1.86v3.516l3.972 2.296v-8.272L4.088 8.481zm16.739 0l-7.317 4.157v8.272l3.972-2.296V15.1l3.345-1.861V8.48zM5.134 6.601l7.303 4.144 7.32-4.18-3.871-2.197-3.41 1.945-3.43-1.968L5.133 6.6z" />
+              </svg>
+            </div>
+          )}
+        </div>
+      </HTMLContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      size: [w, h],
+    } = this.props
+    return <rect width={w} height={h} fill="transparent" />
+  })
+
+  validateProps = (props: Partial<CodeSandboxShapeProps>) => {
+    if (props.size !== undefined) {
+      props.size[0] = Math.max(props.size[0], 1)
+      props.size[1] = Math.max(props.size[1], 1)
+    }
+    return withClampedStyles(props)
+  }
+}

+ 53 - 0
tldraw/tldraw-logseq/src/lib/shapes/DotShape.tsx

@@ -0,0 +1,53 @@
+import * as React from 'react'
+import { TLDotShape, TLDotShapeProps } from '@tldraw/core'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+export interface DotShapeProps extends TLDotShapeProps, CustomStyleProps {
+  type: 'dot'
+}
+
+export class DotShape extends TLDotShape<DotShapeProps> {
+  static id = 'dot'
+
+  static defaultProps: DotShapeProps = {
+    id: 'dot',
+    parentId: 'page',
+    type: 'dot',
+    point: [0, 0],
+    radius: 4,
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  ReactComponent = observer(({ events, isErasing }: TLComponentProps) => {
+    const { radius, stroke, fill, strokeWidth, opacity } = this.props
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <circle className="tl-hitarea-fill" cx={radius} cy={radius} r={radius} />
+        <circle
+          cx={radius}
+          cy={radius}
+          r={radius}
+          stroke={stroke}
+          fill={fill}
+          strokeWidth={strokeWidth}
+          pointerEvents="none"
+        />
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const { radius } = this.props
+    return <circle cx={radius} cy={radius} r={radius} pointerEvents="all" />
+  })
+
+  validateProps = (props: Partial<DotShapeProps>) => {
+    if (props.radius !== undefined) props.radius = Math.max(props.radius, 1)
+    return withClampedStyles(props)
+  }
+}

+ 74 - 0
tldraw/tldraw-logseq/src/lib/shapes/EllipseShape.tsx

@@ -0,0 +1,74 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { TLEllipseShapeProps, TLEllipseShape } from '@tldraw/core'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+export interface EllipseShapeProps extends TLEllipseShapeProps, CustomStyleProps {
+  type: 'ellipse'
+  size: number[]
+}
+
+export class EllipseShape extends TLEllipseShape<EllipseShapeProps> {
+  static id = 'ellipse'
+
+  static defaultProps: EllipseShapeProps = {
+    id: 'ellipse',
+    parentId: 'page',
+    type: 'ellipse',
+    point: [0, 0],
+    size: [100, 100],
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  ReactComponent = observer(({ isSelected, isErasing, events }: TLComponentProps) => {
+    const {
+      size: [w, h],
+      stroke,
+      fill,
+      strokeWidth,
+      opacity,
+    } = this.props
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <ellipse
+          className={isSelected ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
+          cx={w / 2}
+          cy={h / 2}
+          rx={Math.max(0.01, (w - strokeWidth) / 2)}
+          ry={Math.max(0.01, (h - strokeWidth) / 2)}
+        />
+        <ellipse
+          cx={w / 2}
+          cy={h / 2}
+          rx={Math.max(0.01, (w - strokeWidth) / 2)}
+          ry={Math.max(0.01, (h - strokeWidth) / 2)}
+          strokeWidth={strokeWidth}
+          stroke={stroke}
+          fill={fill}
+        />
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      size: [w, h],
+    } = this.props
+    return (
+      <ellipse cx={w / 2} cy={h / 2} rx={w / 2} ry={h / 2} strokeWidth={2} fill="transparent" />
+    )
+  })
+
+  validateProps = (props: Partial<EllipseShapeProps>) => {
+    if (props.size !== undefined) {
+      props.size[0] = Math.max(props.size[0], 1)
+      props.size[1] = Math.max(props.size[1], 1)
+    }
+    return withClampedStyles(props)
+  }
+}

+ 71 - 0
tldraw/tldraw-logseq/src/lib/shapes/HighlighterShape.tsx

@@ -0,0 +1,71 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { SvgPathUtils, TLDrawShape, TLDrawShapeProps } from '@tldraw/core'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { computed, makeObservable } from 'mobx'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+export interface HighlighterShapeProps extends TLDrawShapeProps, CustomStyleProps {
+  type: 'highlighter'
+}
+
+export class HighlighterShape extends TLDrawShape<HighlighterShapeProps> {
+  constructor(props = {} as Partial<HighlighterShapeProps>) {
+    super(props)
+    makeObservable(this)
+  }
+
+  static id = 'highlighter'
+
+  static defaultProps: HighlighterShapeProps = {
+    id: 'highlighter',
+    parentId: 'page',
+    type: 'highlighter',
+    point: [0, 0],
+    points: [],
+    isComplete: false,
+    stroke: '#ffcc00',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  @computed get pointsPath() {
+    const { points } = this.props
+    return SvgPathUtils.getCurvedPathForPoints(points)
+  }
+
+  ReactComponent = observer(({ events, isErasing }: TLComponentProps) => {
+    const {
+      pointsPath,
+      props: { stroke, strokeWidth, opacity },
+    } = this
+
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <path
+          d={pointsPath}
+          strokeWidth={strokeWidth * 16}
+          stroke={stroke}
+          fill="none"
+          pointerEvents="all"
+          strokeLinejoin="round"
+          strokeLinecap="round"
+          opacity={0.5}
+        />
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const { pointsPath } = this
+    return <path d={pointsPath} fill="none" />
+  })
+
+  validateProps = (props: Partial<HighlighterShapeProps>) => {
+    props = withClampedStyles(props)
+    if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
+    return props
+  }
+}

+ 78 - 0
tldraw/tldraw-logseq/src/lib/shapes/ImageShape.tsx

@@ -0,0 +1,78 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { HTMLContainer, TLComponentProps } from '@tldraw/react'
+import { TLImageShape, TLImageShapeProps } from '@tldraw/core'
+import { observer } from 'mobx-react-lite'
+import type { CustomStyleProps } from './style-props'
+
+export interface ImageShapeProps extends TLImageShapeProps, CustomStyleProps {
+  type: 'image'
+  assetId: string
+  opacity: number
+}
+
+export class ImageShape extends TLImageShape<ImageShapeProps> {
+  static id = 'image'
+
+  static defaultProps: ImageShapeProps = {
+    id: 'image1',
+    parentId: 'page',
+    type: 'image',
+    point: [0, 0],
+    size: [100, 100],
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+    assetId: '',
+    clipping: 0,
+    objectFit: 'fill',
+    isAspectRatioLocked: true,
+  }
+
+  ReactComponent = observer(({ events, isErasing, asset }: TLComponentProps) => {
+    const {
+      props: {
+        opacity,
+        objectFit,
+        clipping,
+        size: [w, h],
+      },
+    } = this
+
+    const [t, r, b, l] = Array.isArray(clipping)
+      ? clipping
+      : [clipping, clipping, clipping, clipping]
+
+    return (
+      <HTMLContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <div style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
+          {asset && (
+            <img
+              src={asset.src}
+              draggable={false}
+              style={{
+                position: 'relative',
+                top: -t,
+                left: -l,
+                width: w + (l - r),
+                height: h + (t - b),
+                objectFit,
+                pointerEvents: 'all',
+              }}
+            />
+          )}
+        </div>
+      </HTMLContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      props: {
+        size: [w, h],
+      },
+    } = this
+    return <rect width={w} height={h} fill="transparent" />
+  })
+}

+ 64 - 0
tldraw/tldraw-logseq/src/lib/shapes/LineShape.tsx

@@ -0,0 +1,64 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { TLHandle, TLLineShapeProps, TLLineShape } from '@tldraw/core'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+interface LineShapeProps extends CustomStyleProps, TLLineShapeProps {
+  type: 'line'
+  handles: TLHandle[]
+}
+
+export class LineShape extends TLLineShape<LineShapeProps> {
+  static id = 'line'
+
+  static defaultProps: LineShapeProps = {
+    id: 'line',
+    parentId: 'page',
+    type: 'line',
+    point: [0, 0],
+    handles: [
+      { id: 'start', point: [0, 0] },
+      { id: 'end', point: [1, 1] },
+    ],
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  hideSelection = true
+
+  ReactComponent = observer(({ events, isErasing, isSelected }: TLComponentProps) => {
+    const {
+      points,
+      props: { stroke, fill, strokeWidth, opacity },
+    } = this
+    const path = points.join()
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <g>
+          <polygon className={isSelected ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'} points={path} />
+          <polygon
+            points={path}
+            stroke={stroke}
+            fill={fill}
+            strokeWidth={strokeWidth}
+            strokeLinejoin="round"
+          />
+        </g>
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const { points } = this
+    const path = points.join()
+    return <polygon points={path} />
+  })
+
+  validateProps = (props: Partial<LineShapeProps>) => {
+    return withClampedStyles(props)
+  }
+}

+ 182 - 0
tldraw/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -0,0 +1,182 @@
+// TODO: provide "frontend.components.page/page" component?
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
+import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+import { TextInput } from '~components/inputs/TextInput'
+import { LogseqContext } from '~lib/logseq-context'
+
+export interface LogseqPortalShapeProps extends TLBoxShapeProps, CustomStyleProps {
+  type: 'logseq-portal'
+  pageId: string // page name or UUID
+}
+
+export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
+  static id = 'logseq-portal'
+
+  static defaultProps: LogseqPortalShapeProps = {
+    id: 'logseq-portal',
+    type: 'logseq-portal',
+    parentId: 'page',
+    point: [0, 0],
+    size: [600, 320],
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+    pageId: '',
+  }
+
+  canChangeAspectRatio = true
+  canFlip = false
+  canEdit = false
+
+  ReactContextBar = observer(() => {
+    const { pageId } = this.props
+    const [q, setQ] = React.useState(pageId)
+    const rInput = React.useRef<HTMLInputElement>(null)
+    const { search } = React.useContext(LogseqContext)
+    const app = useApp()
+
+    const secretPrefix = 'œ::'
+
+    const commitChange = React.useCallback((id: string) => {
+      setQ(id)
+      this.update({ pageId: id, size: LogseqPortalShape.defaultProps.size })
+      app.persist()
+      rInput.current?.blur()
+    }, [])
+
+    const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+      const _q = e.currentTarget.value
+      if (_q.startsWith(secretPrefix)) {
+        const id = _q.substring(secretPrefix.length)
+        commitChange(id)
+      } else {
+        setQ(_q)
+      }
+    }, [])
+
+    const options = React.useMemo(() => {
+      if (search && q) {
+        return search(q)
+      }
+      return null
+    }, [search, q])
+
+    return (
+      <>
+        <TextInput
+          ref={rInput}
+          label="Page name or block UUID"
+          type="text"
+          value={q}
+          onChange={handleChange}
+          list="logseq-portal-search-results"
+        />
+        <datalist id="logseq-portal-search-results">
+          {options?.map(option => (
+            <option key={option} value={secretPrefix + option}>
+              {option}
+            </option>
+          ))}
+        </datalist>
+      </>
+    )
+  })
+
+  ReactComponent = observer(({ events, isEditing, isErasing }: TLComponentProps) => {
+    const {
+      props: { opacity, pageId },
+    } = this
+
+    const app = useApp()
+    const { Page } = React.useContext(LogseqContext)
+    const isSelected = app.selectedIds.has(this.id)
+
+    if (!Page) {
+      return null
+    }
+
+    return (
+      <HTMLContainer
+        style={{
+          overflow: 'hidden',
+          pointerEvents: 'all',
+          opacity: isErasing ? 0.2 : opacity,
+          border: '1px solid rgb(52, 52, 52)',
+          backgroundColor: '#ffffff',
+        }}
+        {...events}
+      >
+        {pageId && (
+          <div
+            style={{
+              height: '32px',
+              width: '100%',
+              background: '#bbb',
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+            }}
+          >
+            {pageId}
+          </div>
+        )}
+        <div
+          style={{
+            width: '100%',
+            height: pageId ? 'calc(100% - 32px)' : '100%',
+            pointerEvents: isSelected ? 'none' : 'all',
+            userSelect: 'none',
+          }}
+        >
+          {pageId ? (
+            <div
+              onPointerDown={e => !isEditing && e.stopPropagation()}
+              onPointerUp={e => !isEditing && e.stopPropagation()}
+              style={{ padding: '0 24px' }}
+            >
+              <Page pageId={pageId} />
+            </div>
+          ) : (
+            <div
+              style={{
+                opacity: isSelected ? 0.5 : 1,
+                width: '100%',
+                height: '100%',
+                display: 'flex',
+                alignItems: 'center',
+                overflow: 'hidden',
+                justifyContent: 'center',
+                padding: 16,
+              }}
+            >
+              LOGSEQ PORTAL PLACEHOLDER
+            </div>
+          )}
+        </div>
+      </HTMLContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      props: {
+        size: [w, h],
+      },
+    } = this
+    return <rect width={w} height={h} fill="transparent" />
+  })
+
+  validateProps = (props: Partial<LogseqPortalShapeProps>) => {
+    if (props.size !== undefined) {
+      props.size[0] = Math.max(props.size[0], 50)
+      props.size[1] = Math.max(props.size[1], 50)
+    }
+    return withClampedStyles(props)
+  }
+}

+ 76 - 0
tldraw/tldraw-logseq/src/lib/shapes/PenShape.tsx

@@ -0,0 +1,76 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { getStroke } from 'perfect-freehand'
+import { SvgPathUtils, TLDrawShape, TLDrawShapeProps } from '@tldraw/core'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { computed, makeObservable } from 'mobx'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+export interface PenShapeProps extends TLDrawShapeProps, CustomStyleProps {
+  type: 'draw'
+}
+
+export class PenShape extends TLDrawShape<PenShapeProps> {
+  constructor(props = {} as Partial<PenShapeProps>) {
+    super(props)
+    makeObservable(this)
+  }
+
+  static id = 'draw'
+
+  static defaultProps: PenShapeProps = {
+    id: 'draw',
+    parentId: 'page',
+    type: 'draw',
+    point: [0, 0],
+    points: [],
+    isComplete: false,
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  @computed get pointsPath() {
+    const {
+      props: { points, isComplete, strokeWidth },
+    } = this
+    if (points.length < 2) {
+      return `M -4, 0
+      a 4,4 0 1,0 8,0
+      a 4,4 0 1,0 -8,0`
+    }
+    const stroke = getStroke(points, { size: 4 + strokeWidth * 2, last: isComplete })
+    return SvgPathUtils.getCurvedPathForPolygon(stroke)
+  }
+
+  ReactComponent = observer(({ events, isErasing }: TLComponentProps) => {
+    const {
+      pointsPath,
+      props: { stroke, strokeWidth, opacity },
+    } = this
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <path
+          d={pointsPath}
+          strokeWidth={strokeWidth}
+          stroke={stroke}
+          fill={stroke}
+          pointerEvents="all"
+        />
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const { pointsPath } = this
+    return <path d={pointsPath} />
+  })
+
+  validateProps = (props: Partial<PenShapeProps>) => {
+    props = withClampedStyles(props)
+    if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
+    return props
+  }
+}

+ 65 - 0
tldraw/tldraw-logseq/src/lib/shapes/PencilShape.tsx

@@ -0,0 +1,65 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { SvgPathUtils, TLDrawShape, TLDrawShapeProps } from '@tldraw/core'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { computed, makeObservable } from 'mobx'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+export interface PencilShapeProps extends TLDrawShapeProps, CustomStyleProps {
+  type: 'pencil'
+}
+
+export class PencilShape extends TLDrawShape<PencilShapeProps> {
+  constructor(props = {} as Partial<PencilShapeProps>) {
+    super(props)
+    makeObservable(this)
+  }
+
+  static id = 'pencil'
+
+  static defaultProps: PencilShapeProps = {
+    id: 'pencil',
+    parentId: 'page',
+    type: 'pencil',
+    point: [0, 0],
+    points: [],
+    isComplete: false,
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  @computed get pointsPath() {
+    const { points } = this.props
+    return SvgPathUtils.getCurvedPathForPoints(points)
+  }
+
+  ReactComponent = observer(({ events, isErasing }: TLComponentProps) => {
+    const {
+      pointsPath,
+      props: { stroke, fill, strokeWidth, opacity },
+    } = this
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <polyline
+          points={pointsPath}
+          stroke={stroke}
+          fill={fill}
+          strokeWidth={strokeWidth}
+          pointerEvents="all"
+        />
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const { pointsPath } = this
+    return <path d={pointsPath} fill="none" />
+  })
+
+  validateProps = (props: Partial<PencilShapeProps>) => {
+    return withClampedStyles(props)
+  }
+}

+ 69 - 0
tldraw/tldraw-logseq/src/lib/shapes/PolygonShape.tsx

@@ -0,0 +1,69 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { TLPolygonShape, TLPolygonShapeProps } from '@tldraw/core'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+interface PolygonShapeProps extends TLPolygonShapeProps, CustomStyleProps {
+  type: 'polygon'
+}
+
+export class PolygonShape extends TLPolygonShape<PolygonShapeProps> {
+  static id = 'polygon'
+
+  static defaultProps: PolygonShapeProps = {
+    id: 'polygon',
+    parentId: 'page',
+    type: 'polygon',
+    point: [0, 0],
+    size: [100, 100],
+    sides: 5,
+    ratio: 1,
+    isFlippedY: false,
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  ReactComponent = observer(({ events, isErasing, isSelected }: TLComponentProps) => {
+    const {
+      offset: [x, y],
+      props: { stroke, fill, strokeWidth, opacity },
+    } = this
+    const path = this.getVertices(strokeWidth / 2).join()
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <g transform={`translate(${x}, ${y})`}>
+          <polygon className={isSelected ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'} points={path} />
+          <polygon
+            points={path}
+            stroke={stroke}
+            fill={fill}
+            strokeWidth={strokeWidth}
+            strokeLinejoin="round"
+          />
+        </g>
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      offset: [x, y],
+      props: { strokeWidth },
+    } = this
+    return (
+      <polygon
+        transform={`translate(${x}, ${y})`}
+        points={this.getVertices(strokeWidth / 2).join()}
+      />
+    )
+  })
+
+  validateProps = (props: Partial<PolygonShapeProps>) => {
+    if (props.sides !== undefined) props.sides = Math.max(props.sides, 3)
+    return withClampedStyles(props)
+  }
+}

+ 60 - 0
tldraw/tldraw-logseq/src/lib/shapes/PolylineShape.tsx

@@ -0,0 +1,60 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { TLPolylineShape, TLPolylineShapeProps } from '@tldraw/core'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+interface PolylineShapeProps extends CustomStyleProps, TLPolylineShapeProps {
+  type: 'polyline'
+}
+
+export class PolylineShape extends TLPolylineShape<PolylineShapeProps> {
+  hideSelection = true
+
+  static id = 'polyline'
+
+  static defaultProps: PolylineShapeProps = {
+    id: 'box',
+    parentId: 'page',
+    type: 'polyline',
+    point: [0, 0],
+    handles: [],
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  ReactComponent = observer(({ events, isErasing }: TLComponentProps) => {
+    const {
+      points,
+      props: { stroke, strokeWidth, opacity },
+    } = this
+    const path = points.join()
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <g>
+          <polyline className={'tl-hitarea-stroke'} points={path} />
+          <polyline
+            points={path}
+            stroke={stroke}
+            fill={'none'}
+            strokeWidth={strokeWidth}
+            strokeLinejoin="round"
+          />
+        </g>
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const { points } = this
+    const path = points.join()
+    return <polyline points={path} />
+  })
+
+  validateProps = (props: Partial<PolylineShapeProps>) => {
+    return withClampedStyles(props)
+  }
+}

+ 75 - 0
tldraw/tldraw-logseq/src/lib/shapes/StarShape.tsx

@@ -0,0 +1,75 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { TLStarShape, TLStarShapeProps } from '@tldraw/core'
+import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+interface StarShapeProps extends CustomStyleProps, TLStarShapeProps {
+  type: 'star'
+}
+
+export class StarShape extends TLStarShape<StarShapeProps> {
+  static id = 'star'
+
+  static defaultProps: StarShapeProps = {
+    id: 'star',
+    parentId: 'page',
+    type: 'star',
+    point: [0, 0],
+    size: [100, 100],
+    sides: 5,
+    ratio: 1,
+    isFlippedY: false,
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  ReactComponent = observer(({ events, isErasing, isSelected }: TLComponentProps) => {
+    const {
+      offset: [x, y],
+      props: { stroke, fill, strokeWidth, opacity },
+    } = this
+
+    const path = this.getVertices(strokeWidth / 2).join()
+
+    return (
+      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <polygon
+          className={isSelected ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
+          transform={`translate(${x}, ${y})`}
+          points={path}
+        />
+        <polygon
+          transform={`translate(${x}, ${y})`}
+          points={path}
+          stroke={stroke}
+          fill={fill}
+          strokeWidth={strokeWidth}
+          strokeLinejoin="round"
+          strokeLinecap="round"
+        />
+      </SVGContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      offset: [x, y],
+      props: { strokeWidth },
+    } = this
+    return (
+      <polygon
+        transform={`translate(${x}, ${y})`}
+        points={this.getVertices(strokeWidth / 2).join()}
+      />
+    )
+  })
+
+  validateProps = (props: Partial<StarShapeProps>) => {
+    if (props.sides !== undefined) props.sides = Math.max(props.sides, 3)
+    return withClampedStyles(props)
+  }
+}

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

@@ -0,0 +1,272 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { HTMLContainer, TLComponentProps, TLTextMeasure } from '@tldraw/react'
+import { TextUtils, TLBounds, TLResizeStartInfo, TLTextShape, TLTextShapeProps } from '@tldraw/core'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+import { NumberInput } from '~components/inputs/NumberInput'
+
+export interface TextShapeProps extends TLTextShapeProps, CustomStyleProps {
+  borderRadius: number
+  fontFamily: string
+  fontSize: number
+  fontWeight: number
+  lineHeight: number
+  padding: number
+  type: 'text'
+}
+
+export class TextShape extends TLTextShape<TextShapeProps> {
+  static id = 'text'
+
+  static defaultProps: TextShapeProps = {
+    id: 'box',
+    parentId: 'page',
+    type: 'text',
+    point: [0, 0],
+    size: [100, 100],
+    isSizeLocked: true,
+    text: '',
+    lineHeight: 1.2,
+    fontSize: 20,
+    fontWeight: 400,
+    padding: 4,
+    fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
+    borderRadius: 0,
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+  }
+
+  ReactComponent = observer(({ events, isErasing, isEditing, onEditingEnd }: TLComponentProps) => {
+    const {
+      props: { opacity, fontFamily, fontSize, fontWeight, lineHeight, text, stroke, padding },
+    } = this
+    const rInput = React.useRef<HTMLTextAreaElement>(null)
+
+    const rIsMounted = React.useRef(false)
+
+    const rInnerWrapper = React.useRef<HTMLDivElement>(null)
+
+    // When the text changes, update the text—and,
+    const handleChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
+      const { isSizeLocked } = this.props
+      const text = TextUtils.normalizeText(e.currentTarget.value)
+      if (isSizeLocked) {
+        this.update({ text, size: this.getAutoSizedBoundingBox({ text }) })
+        return
+      }
+      // If not autosizing, update just the text
+      this.update({ text })
+    }, [])
+
+    const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+      if (e.metaKey) e.stopPropagation()
+      switch (e.key) {
+        case 'Meta': {
+          e.stopPropagation()
+          break
+        }
+        case 'z': {
+          if (e.metaKey) {
+            if (e.shiftKey) {
+              document.execCommand('redo', false)
+            } else {
+              document.execCommand('undo', false)
+            }
+            e.preventDefault()
+          }
+          break
+        }
+        case 'Enter': {
+          if (e.ctrlKey || e.metaKey) {
+            e.currentTarget.blur()
+          }
+          break
+        }
+        case 'Tab': {
+          e.preventDefault()
+          if (e.shiftKey) {
+            TextUtils.unindent(e.currentTarget)
+          } else {
+            TextUtils.indent(e.currentTarget)
+          }
+          this.update({ text: TextUtils.normalizeText(e.currentTarget.value) })
+          break
+        }
+      }
+    }, [])
+
+    const handleBlur = React.useCallback(
+      (e: React.FocusEvent<HTMLTextAreaElement>) => {
+        e.currentTarget.setSelectionRange(0, 0)
+        onEditingEnd?.()
+      },
+      [onEditingEnd]
+    )
+
+    const handleFocus = React.useCallback(
+      (e: React.FocusEvent<HTMLTextAreaElement>) => {
+        if (!isEditing) return
+        if (!rIsMounted.current) return
+        if (document.activeElement === e.currentTarget) {
+          e.currentTarget.select()
+        }
+      },
+      [isEditing]
+    )
+
+    const handlePointerDown = React.useCallback(
+      e => {
+        if (isEditing) e.stopPropagation()
+      },
+      [isEditing]
+    )
+
+    React.useEffect(() => {
+      if (isEditing) {
+        requestAnimationFrame(() => {
+          rIsMounted.current = true
+          const elm = rInput.current
+          if (elm) {
+            elm.focus()
+            elm.select()
+          }
+        })
+      } else {
+        onEditingEnd?.()
+      }
+    }, [isEditing, onEditingEnd])
+
+    React.useLayoutEffect(() => {
+      const { fontFamily, fontSize, fontWeight, lineHeight, padding } = this.props
+      const { width, height } = this.measure.measureText(
+        text,
+        { fontFamily, fontSize, fontWeight, lineHeight },
+        padding
+      )
+      this.update({ size: [width, height] })
+    }, [])
+
+    return (
+      <HTMLContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+        <div
+          ref={rInnerWrapper}
+          className="text-shape-wrapper"
+          data-hastext={!!text}
+          data-isediting={isEditing}
+          style={{
+            fontFamily,
+            fontSize,
+            fontWeight,
+            padding,
+            lineHeight,
+            color: stroke,
+          }}
+        >
+          {isEditing ? (
+            <textarea
+              ref={rInput}
+              className="text-shape-input"
+              name="text"
+              tabIndex={-1}
+              autoComplete="false"
+              autoCapitalize="false"
+              autoCorrect="false"
+              autoSave="false"
+              // autoFocus
+              placeholder=""
+              spellCheck="true"
+              wrap="off"
+              dir="auto"
+              datatype="wysiwyg"
+              defaultValue={text}
+              onFocus={handleFocus}
+              onChange={handleChange}
+              onKeyDown={handleKeyDown}
+              onBlur={handleBlur}
+              onPointerDown={handlePointerDown}
+              // onContextMenu={stopPropagation}
+            />
+          ) : (
+            <>{text}&#8203;</>
+          )}
+        </div>
+      </HTMLContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      props: { borderRadius },
+      bounds,
+    } = this
+    return (
+      <rect
+        width={bounds.width}
+        height={bounds.height}
+        rx={borderRadius}
+        ry={borderRadius}
+        fill="transparent"
+      />
+    )
+  })
+
+  validateProps = (props: Partial<TextShapeProps>) => {
+    if (props.isSizeLocked || this.props.isSizeLocked) {
+      props.size = this.getAutoSizedBoundingBox(props)
+    }
+    return withClampedStyles(props)
+  }
+
+  // Custom
+
+  private measure = new TLTextMeasure()
+
+  getAutoSizedBoundingBox(props = {} as Partial<TextShapeProps>) {
+    const {
+      text = this.props.text,
+      fontFamily = this.props.fontFamily,
+      fontSize = this.props.fontSize,
+      fontWeight = this.props.fontWeight,
+      lineHeight = this.props.lineHeight,
+      padding = this.props.padding,
+    } = props
+    const { width, height } = this.measure.measureText(
+      text,
+      { fontFamily, fontSize, lineHeight, fontWeight },
+      padding
+    )
+    return [width, height]
+  }
+
+  getBounds = (): TLBounds => {
+    const [x, y] = this.props.point
+    const [width, height] = this.props.size
+    return {
+      minX: x,
+      minY: y,
+      maxX: x + width,
+      maxY: y + height,
+      width,
+      height,
+    }
+  }
+
+  onResizeStart = ({ isSingle }: TLResizeStartInfo) => {
+    if (!isSingle) return this
+    this.scale = [...(this.props.scale ?? [1, 1])]
+    return this.update({
+      isSizeLocked: false,
+    })
+  }
+
+  onResetBounds = () => {
+    this.update({
+      size: this.getAutoSizedBoundingBox(),
+      isSizeLocked: true,
+    })
+    return this
+  }
+}

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

@@ -0,0 +1,186 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
+import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+import { TextInput } from '~components/inputs/TextInput'
+
+export interface YouTubeShapeProps extends TLBoxShapeProps, CustomStyleProps {
+  type: 'youtube'
+  embedId: string
+}
+
+export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
+  static id = 'youtube'
+
+  static defaultProps: YouTubeShapeProps = {
+    id: 'youtube',
+    type: 'youtube',
+    parentId: 'page',
+    point: [0, 0],
+    size: [600, 320],
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+    embedId: '',
+  }
+
+  aspectRatio = 480 / 853
+
+  canChangeAspectRatio = false
+
+  canFlip = false
+
+  canEdit = true
+
+  ReactContextBar = observer(() => {
+    const { embedId } = this.props
+    const rInput = React.useRef<HTMLInputElement>(null)
+    const app = useApp()
+    const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+      const url = e.currentTarget.value
+      const match = url.match(
+        /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
+      )
+      const embedId = match?.[1] ?? url ?? ''
+      this.update({ embedId, size: YouTubeShape.defaultProps.size })
+      app.persist()
+    }, [])
+    return (
+      <>
+        <TextInput
+          ref={rInput}
+          label="Youtube Video ID"
+          type="text"
+          value={embedId}
+          onChange={handleChange}
+        />
+      </>
+    )
+  })
+
+  ReactComponent = observer(({ events, isEditing, isErasing }: TLComponentProps) => {
+    const {
+      props: { opacity, embedId },
+    } = this
+    const app = useApp()
+    const isSelected = app.selectedIds.has(this.id)
+    return (
+      <HTMLContainer
+        style={{
+          overflow: 'hidden',
+          pointerEvents: 'all',
+          opacity: isErasing ? 0.2 : opacity,
+        }}
+        {...events}
+      >
+        {embedId && (
+          <div
+            style={{
+              height: '32px',
+              width: '100%',
+              background: '#bbb',
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+            }}
+          >
+            {embedId}
+          </div>
+        )}
+        <div
+          style={{
+            width: '100%',
+            height: embedId ? 'calc(100% - 32px)' : '100%',
+            pointerEvents: isEditing ? 'none' : 'all',
+            userSelect: 'none',
+            position: 'relative',
+          }}
+        >
+          {embedId ? (
+            <div
+              style={{
+                overflow: 'hidden',
+                paddingBottom: '56.25%',
+                position: 'relative',
+                height: 0,
+                opacity: isSelected ? 0.5 : 1,
+              }}
+            >
+              <iframe
+                style={{
+                  left: 0,
+                  top: 0,
+                  height: '100%',
+                  width: '100%',
+                  position: 'absolute',
+                }}
+                width="853"
+                height="480"
+                src={`https://www.youtube.com/embed/${embedId}`}
+                frameBorder="0"
+                allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+                allowFullScreen
+                title="Embedded youtube"
+              />
+            </div>
+          ) : (
+            <div
+              style={{
+                width: '100%',
+                height: '100%',
+                display: 'flex',
+                alignItems: 'center',
+                overflow: 'hidden',
+                justifyContent: 'center',
+                backgroundColor: '#ffffff',
+                border: '1px solid rgb(52, 52, 52)',
+                padding: 16,
+              }}
+            >
+              <svg
+                xmlns="http://www.w3.org/2000/svg"
+                viewBox="0 0 502 210.649"
+                height="210.65"
+                width="128"
+              >
+                <g>
+                  <path
+                    d="M498.333 45.7s-2.91-20.443-11.846-29.447C475.157 4.44 462.452 4.38 456.627 3.687c-41.7-3-104.25-3-104.25-3h-.13s-62.555 0-104.255 3c-5.826.693-18.523.753-29.86 12.566-8.933 9.004-11.84 29.447-11.84 29.447s-2.983 24.003-2.983 48.009v22.507c0 24.006 2.983 48.013 2.983 48.013s2.907 20.44 11.84 29.446c11.337 11.817 26.23 11.44 32.86 12.677 23.84 2.28 101.315 2.983 101.315 2.983s62.62-.094 104.32-3.093c5.824-.694 18.527-.75 29.857-12.567 8.936-9.006 11.846-29.446 11.846-29.446s2.98-24.007 2.98-48.013V93.709c0-24.006-2.98-48.01-2.98-48.01"
+                    fill="#cd201f"
+                  />
+                  <g>
+                    <path d="M187.934 169.537h-18.96V158.56c-7.19 8.24-13.284 12.4-19.927 12.4-5.826 0-9.876-2.747-11.9-7.717-1.23-3.02-2.103-7.736-2.103-14.663V68.744h18.957v81.833c.443 2.796 1.636 3.823 4.043 3.823 3.63 0 6.913-3.153 10.93-8.817V68.744h18.96v100.793zM102.109 139.597c.996 9.98-2.1 14.93-7.987 14.93s-8.98-4.95-7.98-14.93v-39.92c-1-9.98 2.093-14.657 7.98-14.657 5.89 0 8.993 4.677 7.996 14.657l-.01 39.92zm18.96-37.923c0-10.77-2.164-18.86-5.987-23.95-5.054-6.897-12.973-9.72-20.96-9.72-9.033 0-15.913 2.823-20.957 9.72-3.886 5.09-5.97 13.266-5.97 24.036l-.016 35.84c0 10.71 1.853 18.11 5.736 23.153 5.047 6.873 13.227 10.513 21.207 10.513 7.986 0 16.306-3.64 21.36-10.513 3.823-5.043 5.586-12.443 5.586-23.153v-35.926zM46.223 114.647v54.889h-19.96v-54.89S5.582 47.358 1.314 34.815H22.27L36.277 87.38l13.936-52.566H71.17l-24.947 79.833z" />
+                  </g>
+                  <g fill="#fff">
+                    <path d="M440.413 96.647c0-9.33 2.557-11.874 8.59-11.874 5.99 0 8.374 2.777 8.374 11.997v10.893l-16.964.02V96.647zm35.96 25.986l-.003-20.4c0-10.656-2.1-18.456-5.88-23.5-5.06-6.823-12.253-10.436-21.317-10.436-9.226 0-16.42 3.613-21.643 10.436-3.84 5.044-6.076 13.28-6.076 23.943v34.927c0 10.596 2.46 18.013 6.296 23.003 5.227 6.813 12.42 10.216 21.87 10.216 9.44 0 16.853-3.566 21.85-10.81 2.2-3.196 3.616-6.82 4.226-10.823.164-1.81.64-5.933.64-11.753v-2.827h-18.96c0 7.247.037 11.557-.133 12.54-1.033 4.834-3.623 7.25-8.07 7.25-6.203 0-8.826-4.636-8.76-13.843v-17.923h35.96zM390.513 140.597c0 9.98-2.353 13.806-7.563 13.806-2.973 0-6.4-1.53-9.423-4.553l.02-60.523c3.02-2.98 6.43-4.55 9.403-4.55 5.21 0 7.563 2.93 7.563 12.91v42.91zm2.104-72.453c-6.647 0-13.253 4.087-19.09 11.27l.02-43.603h-17.963V169.54h17.963l.027-10.05c6.036 7.47 12.62 11.333 19.043 11.333 7.193 0 12.45-3.85 14.863-11.267 1.203-4.226 1.993-10.733 1.993-19.956V99.684c0-9.447-1.21-15.907-2.416-19.917-2.41-7.466-7.247-11.623-14.44-11.623M340.618 169.537h-18.956V158.56c-7.193 8.24-13.283 12.4-19.926 12.4-5.827 0-9.877-2.747-11.9-7.717-1.234-3.02-2.107-7.736-2.107-14.663V69.744h18.96v80.833c.443 2.796 1.633 3.823 4.043 3.823 3.63 0 6.913-3.153 10.93-8.817V69.744h18.957v99.793z" />
+                    <path d="M268.763 169.537h-19.956V54.77h-20.956V35.835l62.869-.024v18.96h-21.957v114.766z" />
+                  </g>
+                </g>
+              </svg>
+            </div>
+          )}
+        </div>
+      </HTMLContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      props: {
+        size: [w, h],
+      },
+    } = this
+    return <rect width={w} height={h} fill="transparent" />
+  })
+
+  validateProps = (props: Partial<YouTubeShapeProps>) => {
+    if (props.size !== undefined) {
+      props.size[0] = Math.max(props.size[0], 1)
+      props.size[1] = Math.max(props.size[0] * this.aspectRatio, 1)
+    }
+    return withClampedStyles(props)
+  }
+}

+ 46 - 0
tldraw/tldraw-logseq/src/lib/shapes/index.ts

@@ -0,0 +1,46 @@
+import type { BoxShape } from './BoxShape'
+import type { CodeSandboxShape } from './CodeSandboxShape'
+import type { DotShape } from './DotShape'
+import type { EllipseShape } from './EllipseShape'
+import type { HighlighterShape } from './HighlighterShape'
+import type { ImageShape } from './ImageShape'
+import type { LineShape } from './LineShape'
+import type { PenShape } from './PenShape'
+import type { PolygonShape } from './PolygonShape'
+import type { PolylineShape } from './PolylineShape'
+import type { StarShape } from './StarShape'
+import type { TextShape } from './TextShape'
+import type { YouTubeShape } from './YouTubeShape'
+import type { LogseqPortalShape } from './LogseqPortalShape';
+
+export type Shape =
+  | BoxShape
+  | CodeSandboxShape
+  | DotShape
+  | EllipseShape
+  | HighlighterShape
+  | ImageShape
+  | LineShape
+  | LineShape
+  | PenShape
+  | PolygonShape
+  | PolylineShape
+  | StarShape
+  | TextShape
+  | YouTubeShape
+  | LogseqPortalShape
+
+export * from './BoxShape'
+export * from './CodeSandboxShape'
+export * from './DotShape'
+export * from './EllipseShape'
+export * from './HighlighterShape'
+export * from './ImageShape'
+export * from './LineShape'
+export * from './PenShape'
+export * from './PolygonShape'
+export * from './PolylineShape'
+export * from './StarShape'
+export * from './TextShape'
+export * from './YouTubeShape'
+export * from './LogseqPortalShape'

+ 24 - 0
tldraw/tldraw-logseq/src/lib/shapes/style-props.tsx

@@ -0,0 +1,24 @@
+export interface CustomStyleProps {
+  stroke: string
+  fill: string
+  strokeWidth: number
+  opacity: number
+}
+
+export function withDefaultStyles<P>(props: P & Partial<CustomStyleProps>): P & CustomStyleProps {
+  return Object.assign(
+    {
+      stroke: '#000000',
+      fill: '#ffffff',
+      strokeWidth: 2,
+      opacity: 1,
+    },
+    props
+  )
+}
+
+export function withClampedStyles<P>(props: P & Partial<CustomStyleProps>) {
+  if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
+  if (props.opacity !== undefined) props.opacity = Math.min(1, Math.max(props.opacity, 0))
+  return props
+}

+ 9 - 0
tldraw/tldraw-logseq/src/lib/tools/BoxTool.tsx

@@ -0,0 +1,9 @@
+import { TLBoxTool } from '@tldraw/core'
+import type { TLReactEventMap } from '@tldraw/react'
+import { Shape, BoxShape } from '~lib'
+
+export class BoxTool extends TLBoxTool<BoxShape, Shape, TLReactEventMap> {
+  static id = 'box'
+  static shortcut = ['r']
+  Shape = BoxShape
+}

+ 9 - 0
tldraw/tldraw-logseq/src/lib/tools/CodeSandboxTool.tsx

@@ -0,0 +1,9 @@
+import { TLBoxTool } from '@tldraw/core'
+import type { TLReactEventMap } from '@tldraw/react'
+import { Shape, CodeSandboxShape } from '~lib/shapes'
+
+export class CodeSandboxTool extends TLBoxTool<CodeSandboxShape, Shape, TLReactEventMap> {
+  static id = 'code'
+  static shortcut = ['x']
+  Shape = CodeSandboxShape
+}

+ 9 - 0
tldraw/tldraw-logseq/src/lib/tools/DotTool.tsx

@@ -0,0 +1,9 @@
+import { TLDotTool } from '@tldraw/core'
+import type { TLReactEventMap } from '@tldraw/react'
+import { Shape, DotShape } from '~lib/shapes'
+
+export class DotTool extends TLDotTool<DotShape, Shape, TLReactEventMap> {
+  static id = 'dot'
+  static shortcut = ['t']
+  Shape = DotShape
+}

+ 9 - 0
tldraw/tldraw-logseq/src/lib/tools/EllipseTool.tsx

@@ -0,0 +1,9 @@
+import { TLBoxTool } from '@tldraw/core'
+import type { TLReactEventMap } from '@tldraw/react'
+import { EllipseShape, Shape } from '~lib'
+
+export class EllipseTool extends TLBoxTool<EllipseShape, Shape, TLReactEventMap> {
+  static id = 'ellipse'
+  static shortcut = ['o']
+  Shape = EllipseShape
+}

+ 8 - 0
tldraw/tldraw-logseq/src/lib/tools/EraseTool.tsx

@@ -0,0 +1,8 @@
+import { TLEraseTool } from '@tldraw/core'
+import type { TLReactEventMap } from '@tldraw/react'
+import type { Shape } from '~lib'
+
+export class NuEraseTool extends TLEraseTool<Shape, TLReactEventMap> {
+  static id = 'erase'
+  static shortcut = ['e']
+}

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

@@ -0,0 +1,11 @@
+import { TLDrawTool } from '@tldraw/core'
+import type { TLReactEventMap } from '@tldraw/react'
+import { HighlighterShape, Shape } from '~lib'
+
+export class HighlighterTool extends TLDrawTool<HighlighterShape, Shape, TLReactEventMap> {
+  static id = 'highlighter'
+  static shortcut = ['h']
+  Shape = HighlighterShape
+  simplify = true
+  simplifyTolerance = 0.618
+}

+ 9 - 0
tldraw/tldraw-logseq/src/lib/tools/LineTool.tsx

@@ -0,0 +1,9 @@
+import { TLLineTool } from '@tldraw/core'
+import type { TLReactEventMap } from '@tldraw/react'
+import { Shape, LineShape } from '~lib'
+
+export class LineTool extends TLLineTool<LineShape, Shape, TLReactEventMap> {
+  static id = 'line'
+  static shortcut = ['l']
+  Shape = LineShape
+}

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

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

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików