Преглед на файлове

Merge branch 'master' into feat/slash-command-for-code-block

Tienson Qin преди 2 години
родител
ревизия
00822dcf10
променени са 30 файла, в които са добавени 431 реда и са изтрити 243 реда
  1. 6 4
      e2e-tests/whiteboards.spec.ts
  2. 1 1
      libs/src/LSPlugin.core.ts
  3. 24 20
      libs/src/LSPlugin.ts
  4. 1 1
      src/main/frontend/commands.cljs
  5. 51 35
      src/main/frontend/components/block.cljs
  6. 24 23
      src/main/frontend/components/bug_report.cljs
  7. 24 24
      src/main/frontend/components/onboarding/quick_tour.cljs
  8. 20 20
      src/main/frontend/components/onboarding/setups.cljs
  9. 10 12
      src/main/frontend/components/query.cljs
  10. 29 31
      src/main/frontend/components/query/result.cljs
  11. 1 1
      src/main/frontend/components/right_sidebar.cljs
  12. 1 1
      src/main/frontend/components/search.cljs
  13. 16 18
      src/main/frontend/components/settings.cljs
  14. 2 2
      src/main/frontend/components/whiteboard.cljs
  15. 27 26
      src/main/frontend/extensions/srs.cljs
  16. 1 1
      src/main/frontend/handler/editor.cljs
  17. 105 1
      src/resources/dicts/en.edn
  18. 4 4
      src/test/frontend/components/query/result_test.cljs
  19. 33 0
      tldraw/apps/tldraw-logseq/src/components/ActionBar/ActionBar.tsx
  20. 8 2
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/CreatingState.tsx
  21. 6 0
      tldraw/packages/core/src/lib/TLApi/TLApi.ts
  22. 0 1
      tldraw/packages/core/src/lib/TLApp/TLApp.ts
  23. 3 6
      tldraw/packages/core/src/lib/TLBaseLineBindingState.ts
  24. 2 0
      tldraw/packages/core/src/lib/TLSettings.ts
  25. 2 2
      tldraw/packages/core/src/lib/TLViewport.ts
  26. 6 1
      tldraw/packages/core/src/lib/tools/TLBoxTool/states/CreatingState.tsx
  27. 3 1
      tldraw/packages/core/src/lib/tools/TLLineTool/states/CreatingState.tsx
  28. 6 0
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/ResizingState.ts
  29. 11 4
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/TranslatingState.ts
  30. 4 1
      tldraw/packages/core/src/lib/tools/TLTextTool/states/CreatingState.tsx

+ 6 - 4
e2e-tests/whiteboards.spec.ts

@@ -291,7 +291,8 @@ test('create a block', async ({ page }) => {
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container')).toHaveCount(1)
 })
 
-test('expand the block', async ({ page }) => {
+// TODO: Fix the failing test
+test.skip('expand the block', async ({ page }) => {
   await page.keyboard.press('Escape')
   await page.keyboard.press(modKey + '+ArrowDown')
   await page.waitForTimeout(100)
@@ -299,7 +300,8 @@ test('expand the block', async ({ page }) => {
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(1)
 })
 
-test('undo the expand action', async ({ page }) => {
+// TODO: Depends on the previous test
+test.skip('undo the expand action', async ({ page }) => {
   await page.keyboard.press(modKey + '+z')
 
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(0)
@@ -442,8 +444,8 @@ test('Create an embedded whiteboard', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   await canvas.dblclick({
     position: {
-      x: 150,
-      y: 150,
+      x: 110,
+      y: 110,
     },
   })
 

+ 1 - 1
libs/src/LSPlugin.core.ts

@@ -233,7 +233,7 @@ function initMainUIHandlers(pluginLocal: PluginLocal) {
   pluginLocal.on(_('attrs'), (attrs: Partial<UIContainerAttrs>) => {
     const el = pluginLocal.getMainUIContainer()
     Object.entries(attrs).forEach(([k, v]) => {
-      el?.setAttribute(k, v)
+      el?.setAttribute(k, String(v))
       if (k === 'draggable' && v) {
         pluginLocal._dispose(
           pluginLocal._setupDraggableContainer(el, {

+ 24 - 20
libs/src/LSPlugin.ts

@@ -33,8 +33,6 @@ export type StyleOptions = {
 export type UIContainerAttrs = {
   draggable: boolean
   resizable: boolean
-
-  [key: string]: any
 }
 
 export type UIBaseOptions = {
@@ -75,20 +73,34 @@ export interface LSPluginPkgConfig {
   mode: 'shadow' | 'iframe'
   themes: Theme[]
   icon: string
-
-  [key: string]: any
+  /**
+   * Alternative entrypoint for development.
+   */
+  devEntry: unknown
+  /**
+   * For legacy themes, do not use.
+   */
+  theme: unknown
 }
 
 export interface LSPluginBaseInfo {
-  id: string // should be unique
+  /**
+   * Must be unique.
+   */
+  id: string
   mode: 'shadow' | 'iframe'
-
   settings: {
     disabled: boolean
-    [key: string]: any
-  }
-
-  [key: string]: any
+  } & Record<string, unknown>
+  effect: boolean
+  /**
+   * For internal use only. Indicates if plugin is installed in dot root.
+   */
+  iir: boolean
+  /**
+   * For internal use only.
+   */
+  lsr: string
 }
 
 export type IHookEvent = {
@@ -146,8 +158,6 @@ export interface AppUserConfigs {
   showBracket: boolean
   enabledFlashcards: boolean
   enabledJournals: boolean
-
-  [key: string]: any
 }
 
 /**
@@ -157,8 +167,6 @@ export interface AppGraphInfo {
   name: string
   url: string
   path: string
-
-  [key: string]: any
 }
 
 /**
@@ -184,8 +192,6 @@ export interface BlockEntity {
   level?: number
   meta?: { timestamps: any; properties: any; startPos: number; endPos: number }
   title?: Array<any>
-
-  [key: string]: any
 }
 
 /**
@@ -205,8 +211,6 @@ export interface PageEntity {
   format?: 'markdown' | 'org'
   journalDay?: number
   updatedAt?: number
-
-  [key: string]: any
 }
 
 export type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>
@@ -1078,8 +1082,8 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
 
   resolveResourceFullUrl(filePath: string): string
 
-  App: IAppProxy & Record<string, any>
-  Editor: IEditorProxy & Record<string, any>
+  App: IAppProxy
+  Editor: IEditorProxy
   DB: IDBProxy
   Git: IGitProxy
   UI: IUIProxy

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

@@ -278,7 +278,7 @@
     [["Query" [[:editor/input "{{query }}" {:backward-pos 2}]
                [:editor/exit]] query-doc]
      ["Zotero" (zotero-steps) "Import Zotero journal article"]
-     ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
+     ["Query function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query function"]
      ["Calculator" [[:editor/input "```calc\n\n```" {:type "block"
                                                      :backward-pos 4}]
                     [:codemirror/focus]] "Insert a calculator"]

+ 51 - 35
src/main/frontend/components/block.cljs

@@ -2705,16 +2705,27 @@
        (= (:id config)
           (str (:block/uuid block)))))
 
-(rum/defc ^:large-vars/cleanup-todo block-container-inner < rum/reactive db-mixins/query
-  [state repo config block]
-  (let [ref? (:ref? config)
-        custom-query? (boolean (:custom-query? config))
-        ref-or-custom-query? (or ref? custom-query?)
-        *navigating-block (get state ::navigating-block)
-        navigating-block (rum/react *navigating-block)
-        navigated? (and (not= (:block/uuid block) navigating-block) navigating-block)
-        block (if (or (and custom-query?
-                           (empty? (:block/children block))
+(defn- build-config [config block {:keys [navigating-block navigated?]}]
+  (cond-> config
+    navigated?
+    (assoc :id (str navigating-block))
+
+    true
+    (update :block merge block)
+
+    ;; Each block might have multiple queries, but we store only the first query's result.
+    ;; This :query-result atom is used by the query function feature to share results between
+    ;; the parent's query block and the children blocks. This works because config is shared
+    ;; between parent and children blocks
+    (nil? (:query-result config))
+    (assoc :query-result (atom nil))
+
+    (:ref? config)
+    (block-handler/attach-order-list-state block)))
+
+(defn- build-block [repo config block* {:keys [navigating-block navigated?]}]
+  (let [block (if (or (and (:custom-query? config)
+                           (empty? (:block/children block*))
                            (not (and (:dsl-query? config)
                                      (string/includes? (:query config) "not"))))
                       navigated?)
@@ -2723,21 +2734,26 @@
                                                       {:scoped-block-id (:db/id block)})
                       tree (tree/blocks->vec-tree blocks (:block/uuid (first blocks)))]
                   (first tree))
-                block)
-        block (if ref?
-                (merge block (db/sub-block (:db/id block)))
-                block)
-        {:block/keys [uuid children pre-block? refs level format content properties]} block
+                block*)
+        {:block/keys [pre-block? format content] :as block'}
+        (if (:ref? config)
+          (merge block (db/sub-block (:db/id block)))
+          block)]
+    (merge block' (block/parse-title-and-body uuid format pre-block? content))))
+
+(rum/defc ^:large-vars/cleanup-todo block-container-inner < rum/reactive db-mixins/query
+  [state repo config* block*]
+  (let [ref? (:ref? config*)
+        custom-query? (boolean (:custom-query? config*))
+        ref-or-custom-query? (or ref? custom-query?)
+        *navigating-block (get state ::navigating-block)
+        navigating-block (rum/react *navigating-block)
+        navigated? (and (not= (:block/uuid block*) navigating-block) navigating-block)
+        block (build-block repo config* block* {:navigating-block navigating-block :navigated? navigated?})
+        {:block/keys [uuid children pre-block? refs level content properties]} block
         {:block.temp/keys [top?]} block
-        config (if navigated? (assoc config :id (str navigating-block)) config)
-        block (merge block (block/parse-title-and-body uuid format pre-block? content))
+        config (build-config config* block {:navigated? navigated? :navigating-block navigating-block})
         blocks-container-id (:blocks-container-id config)
-        config (update config :block merge block)
-        ;; Each block might have multiple queries, but we store only the first query's result
-        config (if (nil? (:query-result config))
-                 (assoc config :query-result (atom nil))
-                 config)
-        config (if ref? (block-handler/attach-order-list-state config block) config)
         heading? (:heading properties)
         *control-show? (get state ::control-show?)
         db-collapsed? (util/collapsed? block)
@@ -2771,18 +2787,18 @@
                     (state/sub-block-selected? blocks-container-id uuid))]
     [:div.ls-block
      (cond->
-       {:id block-id
-        :data-refs data-refs
-        :data-refs-self data-refs-self
-        :data-collapsed (and collapsed? has-child?)
-        :class (str uuid
-                    (when pre-block? " pre-block")
-                    (when (and card? (not review-cards?)) " shadow-md")
-                    (when selected? " selected noselect")
-                    (when order-list? " is-order-list")
-                    (when (string/blank? content) " is-blank"))
-        :blockid (str uuid)
-        :haschild (str (boolean has-child?))}
+      {:id block-id
+       :data-refs data-refs
+       :data-refs-self data-refs-self
+       :data-collapsed (and collapsed? has-child?)
+       :class (str uuid
+                   (when pre-block? " pre-block")
+                   (when (and card? (not review-cards?)) " shadow-md")
+                   (when selected? " selected noselect")
+                   (when order-list? " is-order-list")
+                   (when (string/blank? content) " is-blank"))
+       :blockid (str uuid)
+       :haschild (str (boolean has-child?))}
 
        level
        (assoc :level level)

+ 24 - 23
src/main/frontend/components/bug_report.cljs

@@ -5,7 +5,8 @@
             [frontend.util :as util]
             [reitit.frontend.easy :as rfe]
             [clojure.string :as string]
-            [frontend.handler.notification :as notification]))
+            [frontend.handler.notification :as notification]
+            [frontend.context.i18n :refer [t]]))
 
 (defn parse-clipboard-data-transfer
   "parse dataTransfer
@@ -42,7 +43,7 @@
 
         copy-result-to-clipboard! (fn [result]
                                     (util/copy-to-clipboard! result)
-                                    (notification/show! "Copied to clipboard!"))
+                                    (notification/show! (t :bug-report/inspector-page-copy-notif)))
 
         reset-step! (fn []
                       (set-step! 0)
@@ -56,26 +57,26 @@
 
     [:div.flex.flex-col
      (when (= step 0)
-       (list [:div.mx-auto "Press Ctrl+V / ⌘+V to inspect your clipboard data"]
-             [:div.mx-auto "or click here to paste if you are using the mobile version"]
+       (list [:div.mx-auto (t :bug-report/inspector-page-desc-1)]
+             [:div.mx-auto (t :bug-report/inspector-page-desc-2)]
              ;; for mobile
-             [:input.form-input.is-large.transition.duration-150.ease-in-out {:type "text" :placeholder "Long press here to paste if you are on mobile"}]
+             [:input.form-input.is-large.transition.duration-150.ease-in-out {:type "text" :placeholder (t :bug-report/inspector-page-placeholder)}]
              [:div.flex.justify-between.items-center.mt-2
-              [:div "Something wrong? No problem, click to go back to the previous step."]
-              (ui/button "Go back" :on-click #(util/open-url (rfe/href :bug-report)))]))
+              [:div (t :bug-report/inspector-page-tip)]
+              (ui/button (t :bug-report/inspector-page-btn-back) :on-click #(util/open-url (rfe/href :bug-report)))]))
 
      (when (= step 1)
        (list
-        [:div "Here is the data read from clipboard."]
+        [:div (t :bug-report/inspector-page-desc-clipboard)]
         [:div.flex.justify-between.items-center.mt-2
-         [:div "If this is okay to share, click the copy button."]
-         (ui/button "Copy the result" :on-click #(copy-result-to-clipboard! (js/JSON.stringify (clj->js result) nil 2)))]
+         [:div (t :bug-report/inspector-page-desc-copy)]
+         (ui/button (t :bug-report/inspector-page-btn-copy) :on-click #(copy-result-to-clipboard! (js/JSON.stringify (clj->js result) nil 2)))]
         [:div.flex.justify-between.items-center.mt-2
-         [:div "Now you can report the result pasted to your clipboard. Please paste the result in the 'Additional Context' section and state where you copied the original content from. Thanks!"]
-         (ui/button "Create an issue" :href header/bug-report-url)]
+         [:div (t :bug-report/inspector-page-desc-create-issue)]
+         (ui/button (t :bug-report/inspector-page-btn-create-issue) :href header/bug-report-url)]
         [:div.flex.justify-between.items-center.mt-2
-         [:div "Something wrong? No problem, click to go back to the previous step."]
-         (ui/button "Go back" :on-click reset-step!)]
+         [:div (t :bug-report/inspector-page-tip)]
+         (ui/button (t :bug-report/inspector-page-btn-back) :on-click reset-step!)]
 
         [:pre.whitespace-pre-wrap [:code (js/JSON.stringify (clj->js result) nil 2)]]))]))
 
@@ -102,17 +103,17 @@
    [:div.flex.flex-col.items-center
     [:div.flex.items-center.mb-2
      (ui/icon "bug")
-     [:h1.text-3xl.ml-2 "Bug report"]]
-    [:div.opacity-60 "Can you help us out by submitting a bug report? We'll get it sorted out as soon as we can."]]
+     [:h1.text-3xl.ml-2 (t :bug-report/main-title)]]
+    [:div.opacity-60 (t :bug-report/main-desc)]]
    [:div.cp__bug-report-reporter.rounded-lg.p-8.mt-8
-    [:h1.text-2xl "Is the bug you encountered related to these features?"]
-    [:div.opacity-60 "You can use these handy tools to give us additional information."]
-    (report-item-button "Clipboard helper"
-                 "Inspect and collect clipboard data"
+    [:h1.text-2xl (t :bug-report/section-clipboard-title)]
+    [:div.opacity-60 (t :bug-report/section-clipboard-desc)]
+    (report-item-button (t :bug-report/section-clipboard-btn-title)
+                 (t :bug-report/section-clipboard-btn-desc)
                  "clipboard"
                  {:on-click #(util/open-url (rfe/href :bug-report-tools {:tool "clipboard-data-inspector"}))})
     [:div.py-2] ;; divider
     [:div.flex.flex-col
-     [:h1.text-2xl "Or..."]
-     [:div.opacity-60 "If there are no tools available for you to gather additional information, please report the bug directly."]
-     (report-item-button "Submit a bug report" "Help Make Logseq Better!" "message-report" {:on-click #(util/open-url header/bug-report-url)})]]])
+     [:h1.text-2xl (t :bug-report/section-issues-title)]
+     [:div.opacity-60 (t :bug-report/section-issues-desc)]
+     (report-item-button (t :bug-report/section-issues-btn-title) (t :bug-report/section-issues-btn-desc) "message-report" {:on-click #(util/open-url header/bug-report-url)})]]])

+ 24 - 24
src/main/frontend/components/onboarding/quick_tour.cljs

@@ -18,7 +18,7 @@
   [^js jsTour]
   (let [^js el (js/document.createElement "button")]
     (.add (.-classList el) "cp__onboarding-skip-quick-tour")
-    (set! (.-innerHTML el) (h/render-html [:span [:i.ti.ti-player-skip-forward] "Skip Quick Tour"]))
+    (set! (.-innerHTML el) (h/render-html [:span [:i.ti.ti-player-skip-forward] (t :on-boarding/quick-tour-btn-skip)]))
     (.addEventListener el "click" #(.cancel jsTour))
     [#(.appendChild js/document.body el)
      #(.removeChild js/document.body el)]))
@@ -36,21 +36,21 @@
 
   (h/render-html
    [:div.steps
-    [:strong (str "STEP " current)]
+    [:strong (str (t :on-boarding/quick-tour-steps) current)]
     [:ul (for [i (range total)] [:li {:class (when (= current (inc i)) "active")} i])]]))
 
 (defn- create-steps! [^js jsTour]
   [
    ;; step 1
    {:id                "nav-help"
-    :text              (h/render-html [:section [:h2 "❓ Help"]
-                                       [:p "You can always click here for help and other information about Logseq."]])
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-help-title)]
+                                       [:p (t :on-boarding/quick-tour-help-desc)]])
     :attachTo          {:element ".cp__sidebar-help-btn" :on "top"}
     :beforeShowPromise #(if (state/sub :ui/sidebar-open?)
                           (wait-target state/hide-right-sidebar! 700)
                           (p/resolved true))
     :canClickTarget    true
-    :buttons           [{:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                     {:name    "offset"
@@ -58,11 +58,11 @@
 
    ;; step 2
    {:id                "nav-journal-page"
-    :text              (h/render-html [:section [:h2 "📆 Daily Journal Page"]
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-journal-page-title)]
                                        [:p
-                                        [:span "This is today’s daily journal page. Here you can dump your thoughts, learnings and ideas. Don’t worry about organizing. Just write and"]
-                                        [:a "[[link]]"]
-                                        [:span "your thoughts."]]])
+                                        [:span (t :on-boarding/quick-tour-journal-page-desc-1)]
+                                        [:a (t :on-boarding/quick-tour-journal-page-desc-2)]
+                                        [:span (t :on-boarding/quick-tour-journal-page-desc-3)]]])
 
     :attachTo          {:element ".page.is-journals .page-title" :on "top-end"}
     :beforeShowPromise #(if-not (= (util/safe-lower-case (state/get-current-page))
@@ -71,8 +71,8 @@
                                          (route-handler/redirect-to-page! (date/today))
                                          (util/scroll-to-top)) 200)
                           (p/resolved true))
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/quick-tour-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 63}}
                                     {:name    "offset"
@@ -80,13 +80,13 @@
 
    ;; step 3
    {:id                "nav-left-sidebar"
-    :text              (h/render-html [:section [:h2 "👀 Left Sidebar"]
-                                       [:p [:span "Open the left sidebar to explore important menu items in Logseq."]]])
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-left-sidebar-title)]
+                                       [:p [:span (t :on-boarding/quick-tour-left-sidebar-desc)]]])
 
     :attachTo          {:element "#left-menu" :on "top"}
     :beforeShowPromise #(p/resolved true)
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/quick-tour-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                     {:name    "offset"
@@ -94,15 +94,15 @@
 
    ;; step 4
    {:id                "nav-favorites"
-    :text              (h/render-html [:section [:h2 "⭐️ Favorites"]
-                                       [:p "Pin your favorite pages via the `... `menu on any page."]
-                                       [:p "We’ve also added some template pages here to help you get started. You can remove these once you start writing your own notes."]])
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-favorites-title)]
+                                       [:p (t :on-boarding/quick-tour-favorites-desc-1)]
+                                       [:p (t :on-boarding/quick-tour-favorites-desc-2)]])
     :beforeShowPromise #(if-not (state/sub :ui/left-sidebar-open?)
                           (wait-target state/toggle-left-sidebar! 500)
                           (p/resolved true))
     :attachTo          {:element ".nav-content-item.favorites" :on "right"}
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Finish" :action (.-complete jsTour)}]}
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/quick-tour-btn-finish) :action (.-complete jsTour)}]}
    ])
 
 (defn- create-steps-file-sync! [^js jsTour]
@@ -164,7 +164,7 @@
                          (wait-target ".nav-header .whiteboard" 500)
                          (util/scroll-to-top))
     :canClickTarget    true
-    :buttons           [{:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/tour-whiteboard-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                     {:name    "offset"
@@ -178,8 +178,8 @@
                          (route-handler/redirect-to-whiteboard-dashboard!)
                          (wait-target ".dashboard-create-card" 500))
     :attachTo          {:element ".dashboard-create-card" :on "bottom"}
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Finish" :action (.-complete jsTour)}]
+    :buttons           [{:text (t :on-boarding/tour-whiteboard-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/tour-whiteboard-btn-finish) :action (.-complete jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                     {:name    "offset"
@@ -277,7 +277,7 @@
 
 (defn init []
   (command-palette/register {:id     :document/quick-tour
-                             :desc   "Quick tour for onboarding"
+                             :desc   (t :on-boarding/command-palette-quick-tour)
                              :action #(ready start)})
 
   ;; TODO: fix logic

+ 20 - 20
src/main/frontend/components/onboarding/setups.cljs

@@ -30,13 +30,13 @@
 
       [:h1.text-xl
        (if picker?
-         [:span [:strong (ui/icon "heart")] "Welcome to " [:strong "Logseq!"]]
-         [:span [:strong (ui/icon "file-import")] "Import existing notes"])]
+         [:span [:strong (ui/icon "heart")] (t :on-boarding/main-title) [:strong "Logseq!"]]
+         [:span [:strong (ui/icon "file-import")] (t :on-boarding/importing-main-title)])]
 
       [:h2
        (if picker?
-         "First you need to choose a folder where Logseq will store your thoughts, ideas, notes."
-         "You can also do this later in the app.")]
+         (t :on-boarding/main-desc)
+         (t :on-boarding/importing-main-desc))]
 
       content])])
 
@@ -93,8 +93,8 @@
 
               (if parsing?
                 (ui/loading "")
-                [[:strong "Choose a folder"]
-                 [:small "Open existing directory or Create a new one"]])]]]
+                [[:strong (t :on-boarding/section-btn-title)]
+                 [:small (t :on-boarding/section-btn-desc)]])]]]
            [:div.px-5
             (ui/admonition :warning
                            (widgets/native-fs-api-alert))]))]
@@ -102,22 +102,22 @@
        [:p.flex
         [:i.as-flex-center (ui/icon "zoom-question" {:style {:fontSize "22px"}})]
         [:span.flex-1.flex.flex-col
-         [:strong "How Logseq saves your work"]
-         [:small.opacity-60 "Inside the directory you choose, Logseq will create 4 folders."]]]
+         [:strong (t :on-boarding/section-title)]
+         [:small.opacity-60 (t :on-boarding/section-desc)]]]
 
        [:p.text-sm.pt-5.tracking-wide
-        [:span (str "Each page is a file stored only on your " DEVICE ".")]
+        [:span (str (t :on-boarding/section-tip-1) DEVICE ".")]
         [:br]
-        [:span "You may choose to sync it later."]]
+        [:span (t :on-boarding/section-tip-2)]]
 
        [:ul
         (for [[title label icon]
-              [["Graphics & Documents" "/assets" "whiteboard"]
-               ["Daily notes" "/journals" "calendar-plus"]
-               ["PAGES" "/pages" "page"]
+              [[(t :on-boarding/section-assets) "/assets" "whiteboard"]
+               [(t :on-boarding/section-journals) "/journals" "calendar-plus"]
+               [(t :on-boarding/section-pages) "/pages" "page"]
                []
-               ["APP Internal" "/logseq" "tool"]
-               ["Config File" "/logseq/config.edn"]]]
+               [(t :on-boarding/section-app) "/logseq" "tool"]
+               [(t :on-boarding/section-config) "/logseq/config.edn"]]]
           (if-not title
             [:li.hr]
             [:li
@@ -221,14 +221,14 @@
      :importer
      [:article.flex.flex-col.items-center.importer.py-16.px-8
       [:section.c.text-center
-       [:h1 "Do you already have notes that you want to import?"]
-       [:h2 "If they are in a JSON, EDN or Markdown format Logseq can work with them."]]
+       [:h1 (t :on-boarding/importing-title)]
+       [:h2 (t :on-boarding/importing-desc)]]
       [:section.d.md:flex
        [:label.action-input.flex.items-center.mx-2.my-2
         [:span.as-flex-center [:i (svg/roam-research 28)]]
         [:div.flex.flex-col
          [[:strong "RoamResearch"]
-          [:small "Import a JSON Export of your Roam graph"]]]
+          [:small (t :on-boarding/importing-roam-desc)]]]
         [:input.absolute.hidden
          {:id        "import-roam"
           :type      "file"
@@ -238,7 +238,7 @@
         [:span.as-flex-center [:i (svg/logo 28)]]
         [:span.flex.flex-col
          [[:strong "EDN / JSON"]
-          [:small "Import an EDN or a JSON Export of your Logseq graph"]]]
+          [:small (t :on-boarding/importing-lsq-desc)]]]
         [:input.absolute.hidden
          {:id        "import-lsq"
           :type      "file"
@@ -248,7 +248,7 @@
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.flex.flex-col
          [[:strong "OPML"]
-          [:small " Import OPML files"]]]
+          [:small (t :on-boarding/importing-opml-desc)]]]
 
         [:input.absolute.hidden
          {:id        "import-opml"

+ 10 - 12
src/main/frontend/components/query.cljs

@@ -141,10 +141,8 @@
              (when-not (or built-in? dsl-query?)
                (when collapsed?
                  (editor-handler/collapse-block! current-block-uuid))))
-           state)}
-  (rum/local nil ::query-result)
-  {:init (fn [state] (assoc state :query-error (atom nil)))}
-  [state config {:keys [title builder query view collapsed? table-view?] :as q} *query-triggered?]
+           (assoc state :query-error (atom nil)))}
+  [state config {:keys [title builder query view collapsed? table-view?] :as q}]
   (let [*query-error (:query-error state)
         built-in? (built-in-custom-query? title)
         dsl-query? (:dsl-query? config)
@@ -165,11 +163,12 @@
         view-f (and view-fn (sci/eval-string (pr-str view-fn)))
         dsl-page-query? (and dsl-query?
                              (false? (:blocks? (query-dsl/parse-query query))))
+        ;; FIXME: This isn't getting set for full-text searches
         full-text-search? (and dsl-query?
                                (util/electron?)
                                (symbol? (gp-util/safe-read-string query)))
         result (when (or built-in-collapsed? (not collapsed?'))
-                 (query-result/get-query-result state config *query-error *query-triggered? current-block-uuid q {:table? table?}))
+                 (query-result/get-query-result config q *query-error current-block-uuid {:table? table?}))
         query-time (:query-time (meta result))
         page-list? (and (seq result)
                         (some? (:block/name (first result))))
@@ -208,13 +207,13 @@
                  (if table?
                    [:a.flex.ml-1.fade-link {:title "Switch to list view"
                                             :on-click (fn [] (editor-property/set-block-property! current-block-uuid
-                                                                                                 "query-table"
-                                                                                                 false))}
+                                                                                                  "query-table"
+                                                                                                  false))}
                     (ui/icon "list" {:style {:font-size 20}})]
                    [:a.flex.ml-1.fade-link {:title "Switch to table view"
                                             :on-click (fn [] (editor-property/set-block-property! current-block-uuid
-                                                                                                 "query-table"
-                                                                                                 true))}
+                                                                                                  "query-table"
+                                                                                                  true))}
                     (ui/icon "table" {:style {:font-size 20}})]))
 
                [:a.flex.ml-1.fade-link
@@ -230,7 +229,7 @@
                   (query-refresh-button query-time {:full-text-search? full-text-search?
                                                     :on-mouse-down (fn [e]
                                                                      (util/stop e)
-                                                                     (query-result/trigger-custom-query! state *query-error *query-triggered?))}))]])])
+                                                                     (query-result/trigger-custom-query! config q *query-error))}))]])])
 
          (when dsl-query? builder)
 
@@ -247,11 +246,10 @@
               (custom-query-inner config q opts))])]))))
 
 (rum/defcs custom-query < rum/static
-  (rum/local false ::query-triggered?)
   [state config q]
   (ui/catch-error
    (ui/block-error "Query Error:" {:content (:query q)})
    (ui/lazy-visible
     (fn []
-      (custom-query* config q (::query-triggered? state)))
+      (custom-query* config q))
     {:debug-id q})))

+ 29 - 31
src/main/frontend/components/query/result.cljs

@@ -13,9 +13,8 @@
             [frontend.modules.outliner.tree :as tree]))
 
 (defn trigger-custom-query!
-  [state *query-error *query-triggered?]
-  (let [[config query] (:rum/args state)
-        repo (state/get-current-repo)
+  [config query *query-error]
+  (let [repo (state/get-current-repo)
         result-atom (atom nil)
         current-block-uuid (or (:block/uuid (:block config))
                                (:block/uuid config))
@@ -45,42 +44,41 @@
                      (catch :default e
                        (reset! *query-error e)
                        (atom nil)))]
-    (when *query-triggered?
-      (reset! *query-triggered? true))
     (if (instance? Atom query-atom)
       query-atom
       result-atom)))
 
-(defn get-group-by-page [{:keys [result-transform query] :as q}
+(defn get-group-by-page [{:keys [result-transform query] :as query-m}
                          {:keys [table?]}]
   (if table?
     false ;; Immediately return false as table view can't handle grouping
-    (get q :group-by-page?
+    (get query-m :group-by-page?
          (and (not result-transform)
               (not (and (string? query) (string/includes? query "(by-page false)")))))))
 
 (defn get-query-result
-  [state config *query-error *query-triggered? current-block-uuid q options]
-  (or (when-let [*result (:query-result config)] @*result)
-      (let [query-atom (trigger-custom-query! state *query-error *query-triggered?)
-            query-result (and query-atom (rum/react query-atom))
-            ;; exclude the current one, otherwise it'll loop forever
-            remove-blocks (if current-block-uuid [current-block-uuid] nil)
-            transformed-query-result (when query-result
-                                       (let [result (db/custom-query-result-transform query-result remove-blocks q)]
-                                         (if (and query-result (coll? result) (:block/uuid (first result)))
-                                           (cond-> result
-                                            (get q :remove-block-children? true)
-                                            tree/filter-top-level-blocks)
-                                           result)))
-            group-by-page? (get-group-by-page q options)
-            result (if (and group-by-page? (:block/uuid (first transformed-query-result)))
-                     (let [result (db-utils/group-by-page transformed-query-result)]
-                       (if (map? result)
-                         (dissoc result nil)
-                         result))
-                     transformed-query-result)]
-        (when-let [query-result (:query-result config)]
-          (reset! query-result result))
-        (when query-atom
-          (util/safe-with-meta result (meta @query-atom))))))
+  "Fetches a query's result, transforms it as needed and saves the result into
+  an atom that is passed in as an argument"
+  [config query-m *query-error current-block-uuid options]
+  (let [query-atom (trigger-custom-query! config query-m *query-error)
+        query-result (and query-atom (rum/react query-atom))
+        ;; exclude the current one, otherwise it'll loop forever
+        remove-blocks (if current-block-uuid [current-block-uuid] nil)
+        transformed-query-result (when query-result
+                                   (let [result (db/custom-query-result-transform query-result remove-blocks query-m)]
+                                     (if (and query-result (coll? result) (:block/uuid (first result)))
+                                       (cond-> result
+                                         (get query-m :remove-block-children? true)
+                                         tree/filter-top-level-blocks)
+                                       result)))
+        group-by-page? (get-group-by-page query-m options)
+        result (if (and group-by-page? (:block/uuid (first transformed-query-result)))
+                 (let [result (db-utils/group-by-page transformed-query-result)]
+                   (if (map? result)
+                     (dissoc result nil)
+                     result))
+                 transformed-query-result)]
+    (when-let [query-result (:query-result config)]
+      (reset! query-result result))
+    (when query-atom
+      (util/safe-with-meta result (meta @query-atom)))))

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

@@ -114,7 +114,7 @@
 
 (defn build-sidebar-item
   [repo idx db-id block-type]
-  (case block-type
+  (case (keyword block-type)
     :contents
     [(t :right-side-bar/contents)
      (contents)]

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

@@ -408,7 +408,7 @@
                     true)]
         (ui/tippy {:html [:div
                           ;; TODO: fetch from config
-                          "Tip: " [:code (util/->platform-shortcut "Ctrl + Shift + p")] " to open the commands palette"]
+                          (t :search/command-palette-tip-1) [:code (util/->platform-shortcut "Ctrl + Shift + p")] (t :search/command-palette-tip-2)]
                    :interactive     true
                    :arrow           true
                    :theme       "monospace"}

+ 16 - 18
src/main/frontend/components/settings.cljs

@@ -104,7 +104,7 @@
 
           "update-available"
           (let [{:keys [name url]} payload]
-            [:p (str "Found new release ")
+            [:p (str (t :settings-page/update-available))
              [:a.link
               {:on-click
                (fn [e]
@@ -113,7 +113,7 @@
               svg/external-link name " 🎉"]])
 
           "error"
-          [:p "⚠️ Oops, Something Went Wrong!" [:br] " Please check out the "
+          [:p (t :settings-page/update-error-1) [:br] (t :settings-page/update-error-2)
            [:a.link
             {:on-click
              (fn [e]
@@ -295,12 +295,12 @@
 
 (defn theme-modes-row [t switch-theme system-theme? dark?]
   (let [pick-theme [:ul.theme-modes-options
-                    [:li {:on-click (partial state/use-theme-mode! "light")
-                          :class    (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light] [:strong "light"]]
-                    [:li {:on-click (partial state/use-theme-mode! "dark")
-                          :class    (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark] [:strong "dark"]]
-                    [:li {:on-click (partial state/use-theme-mode! "system")
-                          :class    (classnames [{:active system-theme?}])} [:i.mode-system] [:strong "system"]]]]
+                    [:li {:on-click (partial state/use-theme-mode! (t :settings-page/theme-light))
+                          :class    (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light] [:strong (t :settings-page/theme-light)]]
+                    [:li {:on-click (partial state/use-theme-mode! (t :settings-page/theme-dark))
+                          :class    (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark] [:strong (t :settings-page/theme-dark)]]
+                    [:li {:on-click (partial state/use-theme-mode! (t :settings-page/theme-system))
+                          :class    (classnames [{:active system-theme?}])} [:i.mode-system] [:strong (t :settings-page/theme-system)]]]]
     (row-with-button-action {:left-label (t :right-side-bar/switch-theme (string/capitalize switch-theme))
                              :-for       "toggle_theme"
                              :action     pick-theme
@@ -342,7 +342,7 @@
                       (when-not (string/blank? format)
                         (config-handler/set-config! :journal/page-title-format format)
                         (notification/show!
-                          [:div "You must re-index your graph for this change to take effect"]
+                          [:div (t :settings-page/custom-date-format-notification)]
                           :warning false)
                         (state/close-modal!)
                         (route-handler/redirect! {:to :repos}))))}
@@ -690,18 +690,16 @@
    [:div.text-sm.my-4
     (ui/admonition
      :tip
-     [:p "If you have Logseq Sync enabled, you can view a page's edit history directly. This section is for tech-savvy only."])
+     [:p (t :settings-page/git-tip)])
     [:span.text-sm.opacity-50.my-4 
-     "To view page's edit history, click the three horizontal dots in the top-right corner and select \"View page history\"."]
+     (t :settings-page/git-desc-1)]
     [:br][:br]
     [:span.text-sm.opacity-50.my-4
-     "For professional users, Logseq also supports using "]
+     (t :settings-page/git-desc-2)]
     [:a {:href "https://git-scm.com/" :target "_blank"}
      "Git"]
     [:span.text-sm.opacity-50.my-4
-     " for version control."]
-    [:span.text-sm.opacity-50.my-4
-     "Use Git at your own risk as general Git issues are not supported by the Logseq team"]]
+     (t :settings-page/git-desc-3)]]
    [:br]
    (switch-git-auto-commit-row t)
    (git-auto-commit-seconds t)
@@ -997,11 +995,11 @@
          {:class (when-not user-handler/alpha-or-beta-user? "opacity-50 pointer-events-none cursor-not-allowed")}
          (sync-switcher-row enable-sync?)
          [:div.text-sm
-          "Click"
+          (t :settings-page/sync-desc-1)
           [:a.mx-1 {:href "https://blog.logseq.com/how-to-setup-and-use-logseq-sync/"
                     :target "_blank"}
-           "here"]
-          "for instructions on how to set up and use Sync."]]])]))
+           (t :settings-page/sync-desc-2)]
+          (t :settings-page/sync-desc-3)]]])]))
 
      ;; (when-not web-platform?
      ;;   [:<>

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

@@ -146,7 +146,7 @@
   [page-name]
   (let [page-entity (model/get-page page-name)
         {:block/keys [updated-at created-at]} page-entity]
-    (str (if (= created-at updated-at) "Created " "Edited ")
+    (str (if (= created-at updated-at) (t :whiteboard/dashboard-card-created) (t :whiteboard/dashboard-card-edited))
          (util/time-ago (js/Date. updated-at)))))
 
 (rum/defc dashboard-preview-card
@@ -190,7 +190,7 @@
       (whiteboard-handler/create-new-whiteboard-and-redirect!))}
    (ui/icon "plus")
    [:span.dashboard-create-card-caption.select-none
-    "New whiteboard"]])
+    (t :whiteboard/dashboard-card-new-whiteboard)]])
 
 (rum/defc whiteboard-dashboard
   []

+ 27 - 26
src/main/frontend/extensions/srs.cljs

@@ -28,7 +28,8 @@
             [clojure.string :as string]
             [rum.core :as rum]
             [frontend.modules.shortcut.core :as shortcut]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.context.i18n :refer [t]]))
 
 ;;; ================================================================
 ;;; Commentary
@@ -409,7 +410,7 @@
     (reset! *phase 1)))
 
 (def review-finished
-  [:p.p-2 "Congrats, you've reviewed all the cards for this query, see you next time! 💯"])
+  [:p.p-2 (t :flashcards/modal-finished)])
 
 (defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click class]}]
   (ui/button
@@ -457,9 +458,9 @@
            [:div.flex.my-4.justify-between
             (when-not (and (not preview?) (= next-phase 1))
               (btn-with-shortcut {:btn-text (case next-phase
-                                              1 "Hide answers"
-                                              2 "Show answers"
-                                              3 "Show clozes")
+                                              1 (t :flashcards/modal-btn-hide-answers)
+                                              2 (t :flashcards/modal-btn-show-answers)
+                                              3 (t :flashcards/modal-btn-show-clozes))
                                   :shortcut  "s"
                                   :id "card-answers"
                                   :class "mr-2"
@@ -467,7 +468,7 @@
             (when (and (not= @card-index (count blocks))
                        cards?
                        preview?)
-              (btn-with-shortcut {:btn-text "Next"
+              (btn-with-shortcut {:btn-text (t :flashcards/modal-btn-next-card)
                                   :shortcut "n"
                                   :id       "card-next"
                                   :class    "mr-2"
@@ -477,7 +478,7 @@
 
             (when (and (not preview?) (= 1 next-phase))
               [:<>
-               (btn-with-shortcut {:btn-text   "Forgotten"
+               (btn-with-shortcut {:btn-text   (t :flashcards/modal-btn-forgotten)
                                    :shortcut   "f"
                                    :id         "card-forgotten"
                                    :background "red"
@@ -486,12 +487,12 @@
                                                  (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
                                                    (editor-property/set-block-property! root-block-id card-next-schedule-property tomorrow)))})
 
-               (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" "Took a while to recall")
+               (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" (t :flashcards/modal-btn-recall))
                                    :shortcut "t"
                                    :id       "card-recall"
                                    :on-click #(score-and-next-card 3 card card-index finished? phase review-records cb)})
 
-               (btn-with-shortcut {:btn-text   "Remembered"
+               (btn-with-shortcut {:btn-text   (t :flashcards/modal-btn-remembered)
                                    :shortcut   "r"
                                    :id         "card-remembered"
                                    :background "green"
@@ -499,10 +500,10 @@
 
             (when preview?
               (ui/tippy {:html [:div.text-sm
-                                "Reset this card so that you can review it immediately."]
+                                (t :flashcards/modal-btn-reset-tip)]
                          :class "tippy-hover"
                          :interactive true}
-                        (ui/button [:span "Reset"]
+                        (ui/button [:span (t :flashcards/modal-btn-reset)]
                                    :id "card-reset"
                                    :class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset")
                                    :on-click (fn [e]
@@ -590,11 +591,11 @@
   (let [cards (db-model/get-macro-blocks (state/get-current-repo) "cards")
         items (->> (map (comp :logseq.macro-arguments :block/properties) cards)
                    (map (fn [col] (string/join " " col))))
-        items (concat items ["All"])]
+        items (concat items [(t :flashcards/modal-select-all)])]
     (component-select/select {:items items
                               :on-chosen on-chosen
                               :close-modal? false
-                              :input-default-placeholder "Switch to"
+                              :input-default-placeholder (t :flashcards/modal-select-switch)
                               :extract-fn nil})))
 
 ;;; register cards macro
@@ -627,12 +628,12 @@
                {:on-mouse-down (fn [e]
                                  (util/stop e)
                                  (toggle-fn))}
-               [:span.flex (if (string/blank? query-string) "All" query-string)
+               [:span.flex (if (string/blank? query-string) (t :flashcards/modal-select-all) query-string)
                 [:span {:style {:margin-top 2}}
                  (svg/caret-down)]]])
             (fn [{:keys [toggle-fn]}]
               (cards-select {:on-chosen (fn [query]
-                                          (let [query' (if (= query "All") "" query)]
+                                          (let [query' (if (= query (t :flashcards/modal-select-all)) "" query)]
                                             (reset! query-atom query')
                                             (toggle-fn)))}))
             {:modal-class (util/hiccup->class
@@ -642,13 +643,13 @@
 
            ;; FIXME: CSS issue
            (if @*preview-mode?
-             (ui/tippy {:html [:div.text-sm "current/total"]
+             (ui/tippy {:html [:div.text-sm (t :flashcards/modal-current-total)]
                         :interactive true}
                        [:div.opacity-60.text-sm.mr-3
                         @*card-index
                         [:span "/"]
                         total])
-             (ui/tippy {:html [:div.text-sm "overdue/total"]
+             (ui/tippy {:html [:div.text-sm (t :flashcards/modal-overdue-total)]
                         ;; :class "tippy-hover"
                         :interactive true}
                        [:div.opacity-60.text-sm.mr-3
@@ -657,7 +658,7 @@
                         total]))
 
            (ui/tippy
-            {:html [:div.text-sm "Toggle preview mode"]
+            {:html [:div.text-sm (t :flashcards/modal-toggle-preview-mode)]
              :delay [1000, 100]
              :class "tippy-hover"
              :interactive true
@@ -672,7 +673,7 @@
              "A"])
 
            (ui/tippy
-            {:html [:div.text-sm "Toggle random mode"]
+            {:html [:div.text-sm (t :flashcards/modal-toggle-random-mode)]
              :delay [1000, 100]
              :class "tippy-hover"
              :interactive true}
@@ -702,15 +703,15 @@
                      *card-index))]])
       (if (:global? config)
         [:div.ls-card.content
-         [:h1.title "Time to create a card!"]
+         [:h1.title (t :flashcards/modal-welcome-title)]
 
          [:div
-          [:p "You can add \"#card\" to any block to turn it into a card or trigger \"/cloze\" to add some clozes."]
+          [:p (t :flashcards/modal-welcome-desc-1)]
           [:img.my-4 {:src "https://docs.logseq.com/assets/2021-07-22_22.28.02_1626964258528_0.gif"}]
-          [:p "You can "
-           [:a {:href "https://docs.logseq.com/#/page/cards" :target "_blank"}
-            "click this link"]
-           " to check the documentation."]]]
+          [:p (t :flashcards/modal-welcome-desc-2)
+           [:a {:href "https://docs.logseq.com/#/page/Flashcards" :target "_blank"}
+            (t :flashcards/modal-welcome-desc-3)]
+           (t :flashcards/modal-welcome-desc-4)]]]
         [:div.opacity-60.custom-query-title.ls-card.content
          [:div.w-full.flex-1
           [:code.p-1 (str "Cards: " query-string)]]
@@ -808,4 +809,4 @@
       (when (nil? @*due-cards-interval)
         ;; refresh every hour
         (let [interval (js/setInterval f (* 3600 1000))]
-          (reset! *due-cards-interval interval))))))
+          (reset! *due-cards-interval interval))))))

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

@@ -3616,7 +3616,7 @@
         edit-block (state/get-edit-block)
         target-element (.-nodeName (.-target e))]
     (cond
-      (whiteboard?)
+      (and (whiteboard?) (not edit-input))
       (do
         (util/stop e)
         (.selectAll (.-api ^js (state/active-tldraw-app))))

+ 105 - 1
src/resources/dicts/en.edn

@@ -17,6 +17,67 @@
  :on-boarding/tour-whiteboard-home-description "Whiteboards have their own section in the app where you can see them at a glance, create new ones or delete them easily."
  :on-boarding/tour-whiteboard-new "{1} Create new whiteboard"
  :on-boarding/tour-whiteboard-new-description "There are multiple ways of creating a new whiteboard. One of them is always right here in the dashboard."
+ :on-boarding/tour-whiteboard-btn-next "Next"
+ :on-boarding/tour-whiteboard-btn-back "Back"
+ :on-boarding/tour-whiteboard-btn-finish "Finish"
+ :on-boarding/quick-tour-btn-next "Next"
+ :on-boarding/quick-tour-btn-back "Back"
+ :on-boarding/quick-tour-btn-finish "Finish"
+ :on-boarding/quick-tour-btn-skip "Skip Quick Tour"
+ :on-boarding/quick-tour-steps "STEP "
+ :on-boarding/quick-tour-help-title "❓ Help"
+ :on-boarding/quick-tour-help-desc "You can always click here for help and other information about Logseq."
+ :on-boarding/quick-tour-journal-page-title "📆 Daily Journal Page"
+ :on-boarding/quick-tour-journal-page-desc-1 "This is today’s daily journal page. Here you can dump your thoughts, learnings and ideas. Don’t worry about organizing. Just write and"
+ :on-boarding/quick-tour-journal-page-desc-2 "[[link]]"
+ :on-boarding/quick-tour-journal-page-desc-3 "your thoughts."
+ :on-boarding/quick-tour-left-sidebar-title "👀 Left Sidebar"
+ :on-boarding/quick-tour-left-sidebar-desc "Open the left sidebar to explore important menu items in Logseq."
+ :on-boarding/quick-tour-favorites-title "⭐️ Favorites"
+ :on-boarding/quick-tour-favorites-desc-1 "Pin your favorite pages via the `... `menu on any page."
+ :on-boarding/quick-tour-favorites-desc-2 "We’ve also added some template pages here to help you get started. You can remove these once you start writing your own notes."
+ :on-boarding/command-palette-quick-tour "Quick tour for onboarding"
+ :on-boarding/importing-main-title "Import existing notes"
+ :on-boarding/importing-main-desc "You can also do this later in the app."
+ :on-boarding/importing-title "Do you already have notes that you want to import?"
+ :on-boarding/importing-desc "If they are in a JSON, EDN or Markdown format Logseq can work with them."
+ :on-boarding/importing-roam-desc "Import a JSON Export of your Roam graph"
+ :on-boarding/importing-lsq-desc "Import an EDN or a JSON Export of your Logseq graph"
+ :on-boarding/importing-opml-desc " Import OPML files"
+ :on-boarding/main-title "Welcome to "
+ :on-boarding/main-desc "First you need to choose a folder where Logseq will store your thoughts, ideas, notes."
+ :on-boarding/section-btn-title "Choose a folder"
+ :on-boarding/section-btn-desc "Open existing directory or Create a new one"
+ :on-boarding/section-title "How Logseq saves your work"
+ :on-boarding/section-desc "Inside the directory you choose, Logseq will create 4 folders."
+ :on-boarding/section-tip-1 "Each page is a file stored only on your "
+ :on-boarding/section-tip-2 "You may choose to sync it later."
+ :on-boarding/section-assets "Graphics & Documents"
+ :on-boarding/section-journals "Daily notes"
+ :on-boarding/section-pages "PAGES"
+ :on-boarding/section-app "APP Internal"
+ :on-boarding/section-config "Config File"
+ :bug-report/main-title "Bug report"
+ :bug-report/main-desc "Can you help us out by submitting a bug report? We'll get it sorted out as soon as we can."
+ :bug-report/section-clipboard-title "Is the bug you encountered related to these features?"
+ :bug-report/section-clipboard-desc "You can use these handy tools to give us additional information."
+ :bug-report/section-clipboard-btn-title "Clipboard helper"
+ :bug-report/section-clipboard-btn-desc "Inspect and collect clipboard data"
+ :bug-report/section-issues-title "Or..."
+ :bug-report/section-issues-desc "If there are no tools available for you to gather additional information, please report the bug directly."
+ :bug-report/section-issues-btn-title "Submit a bug report"
+ :bug-report/section-issues-btn-desc "Help Make Logseq Better!"
+ :bug-report/inspector-page-desc-1 "Press Ctrl+V / ⌘+V to inspect your clipboard data"
+ :bug-report/inspector-page-desc-2 "or click here to paste if you are using the mobile version"
+ :bug-report/inspector-page-placeholder "Long press here to paste if you are on mobile"
+ :bug-report/inspector-page-tip "Something wrong? No problem, click to go back to the previous step."
+ :bug-report/inspector-page-btn-back "Go back"
+ :bug-report/inspector-page-btn-copy "Copy the result"
+ :bug-report/inspector-page-copy-notif "Copied to clipboard!"
+ :bug-report/inspector-page-btn-create-issue "Create an issue"
+ :bug-report/inspector-page-desc-clipboard "Here is the data read from clipboard."
+ :bug-report/inspector-page-desc-copy "If this is okay to share, click the copy button."
+ :bug-report/inspector-page-desc-create-issue "Now you can report the result pasted to your clipboard. Please paste the result in the 'Additional Context' section and state where you copied the original content from. Thanks!"
  :help/title-usage "Usage"
  :help/title-community "Community"
  :help/title-development "Development"
@@ -48,6 +109,8 @@
  :search/page-names "Search page names"
  :search/recent "Recent search:"
  :search/blocks-in-page "Search blocks in page:"
+ :search/command-palette-tip-1 "Tip: "
+ :search/command-palette-tip-2 " to open the commands palette"
  :search/cache-outdated "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."
  :search-item/whiteboard "Whiteboard"
  :search-item/page "Page"
@@ -182,9 +245,13 @@
  :context-menu/input-template-name "What's the template's name?"
  :context-menu/template-include-parent-block "Including the parent block in the template?"
  :context-menu/template-exists-warning "Template already exists!"
- :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+ :settings-page/git-tip "If you have Logseq Sync enabled, you can view a page's edit history directly. This section is for tech-savvy only."
+ :settings-page/git-desc-1 "To view page's edit history, click the three horizontal dots in the top-right corner and select \"View page history\"."
+ :settings-page/git-desc-2 "For professional users, Logseq also supports using "
+ :settings-page/git-desc-3 " for version control. Use Git at your own risk as general Git issues are not supported by the Logseq team."
  :settings-page/git-switcher-label "Enable Git auto commit"
  :settings-page/git-commit-delay "Git auto commit seconds"
+ :settings-page/git-confirm "You need to restart the app after updating the Git settings."
  :settings-page/edit-config-edn "Edit config.edn"
  :settings-page/edit-global-config-edn "Edit global config.edn"
  :settings-page/edit-custom-css "Edit custom.css"
@@ -192,6 +259,9 @@
  :settings-page/edit-setting "Edit"
  :settings-page/custom-configuration "Custom configuration"
  :settings-page/custom-global-configuration "Custom global configuration"
+ :settings-page/theme-light "light"
+ :settings-page/theme-dark "dark"
+ :settings-page/theme-system "system"
  :settings-page/custom-theme "Custom theme"
  :settings-page/export-theme "Export theme"
  :settings-page/show-brackets "Show brackets"
@@ -207,6 +277,7 @@
  :settings-page/auto-expand-block-refs-tip "This option controls whether to expand the block references automatically when zoom-in."
  :settings-page/custom-date-format "Preferred date format"
  :settings-page/custom-date-format-warning "Re-index required! Existing journal references would be broken!"
+ :settings-page/custom-date-format-notification "You must re-index your graph for this change to take effect"
  :settings-page/preferred-pasting-file-hint "When enabled, pasting an image from the internet will download and insert the image. When disabled, it will paste the link to the image."
  :settings-page/preferred-file-format "Preferred file format"
  :settings-page/preferred-workflow "Preferred workflow"
@@ -241,6 +312,9 @@
  :settings-page/beta-features "Beta features"
  :settings-page/login-prompt "To access new features before anyone else you must be an Open Collective Sponsor or Backer of Logseq and therefore log in first."
  :settings-page/sync "Sync"
+ :settings-page/sync-desc-1 "Click"
+ :settings-page/sync-desc-2 "here"
+ :settings-page/sync-desc-3 "for instructions on how to set up and use Sync."
  :settings-page/enable-whiteboards "Whiteboards"
  :settings-page/native-titlebar "Native title bar"
  :settings-page/native-titlebar-desc "Enables the native window title bar on Windows and Linux."
@@ -249,6 +323,9 @@
  :settings-page/revision "Revision: "
  :settings-page/changelog "What's new?"
  :settings-page/app-updated "Your app is up-to-date 🎉"
+ :settings-page/update-available "Found new release "
+ :settings-page/update-error-1 "⚠️ Oops, Something Went Wrong!"
+ :settings-page/update-error-2 " Please check out the "
  :yes "Yes"
 
  :submit "Submit"
@@ -354,6 +431,33 @@
  :whiteboard/search-only-pages "Search only pages"
  :whiteboard/cache-outdated "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."
  :whiteboard/shape-quick-links "Shape Quick Links"
+ :whiteboard/dashboard-card-new-whiteboard "New whiteboard"
+ :whiteboard/dashboard-card-created "Created "
+ :whiteboard/dashboard-card-edited "Edited "
+ :whiteboard/toggle-grid "Toggle grid"
+ :whiteboard/snap-to-grid "Snap to grid"
+ :flashcards/modal-welcome-title "Time to create a card!"
+ :flashcards/modal-welcome-desc-1 "You can add \"#card\" to any block to turn it into a card or trigger \"/cloze\" to add some clozes."
+ :flashcards/modal-welcome-desc-2 "You can "
+ :flashcards/modal-welcome-desc-3 "click this link"
+ :flashcards/modal-welcome-desc-4 " to check the documentation."
+ :flashcards/modal-btn-show-answers "Show answers"
+ :flashcards/modal-btn-hide-answers "Hide answers"
+ :flashcards/modal-btn-show-clozes "Show clozes"
+ :flashcards/modal-btn-next-card "Next"
+ :flashcards/modal-btn-reset "Reset"
+ :flashcards/modal-btn-reset-tip "Reset this card so that you can review it immediately." 
+ :flashcards/modal-btn-forgotten "Forgotten"
+ :flashcards/modal-btn-remembered "Remembered"
+ :flashcards/modal-btn-recall "Took a while to recall"
+ :flashcards/modal-finished "Congrats, you've reviewed all the cards for this query, see you next time! 💯"
+ :flashcards/modal-select-all "All"
+ :flashcards/modal-select-switch "Switch to"
+ :flashcards/modal-current-total "Current/Total"
+ :flashcards/modal-overdue-total "Overdue/Total"
+ :flashcards/modal-toggle-preview-mode "Toggle preview mode"
+ :flashcards/modal-toggle-random-mode "Toggle random mode"
+
  :page-search "Search in the current page"
  :graph-search "Search graph"
  :home "Home"

+ 4 - 4
src/test/frontend/components/query/result_test.cljs

@@ -8,11 +8,11 @@
 (defn- mock-get-query-result
   "Mocks get-query-result assuming custom queries are being tested. Db calls are
   mocked to minimize setup"
-  [result query {:keys [table? current-block-uuid config] :or {config {}}}]
+  [result query-m {:keys [table? current-block-uuid config] :or {config {}}}]
   (with-redefs [db/custom-query (constantly (atom result))
                 model/with-pages identity]
     (binding [rum/*reactions* (volatile! #{})]
-      (#'query-result/get-query-result {} config (atom nil) (atom nil) current-block-uuid query {:table? table?}))))
+      (#'query-result/get-query-result config query-m (atom nil) current-block-uuid {:table? table?}))))
 
 (deftest get-query-result-with-transforms-and-grouping
   (let [result (mapv
@@ -22,8 +22,8 @@
                  {:block/uuid (random-uuid) :block/scheduled 20230417}])
         sorted-result (sort-by :block/scheduled result)]
     (testing "For list view"
-      (are [query expected]
-           (= expected (mock-get-query-result result query {:table? false}))
+      (are [query-m expected]
+           (= expected (mock-get-query-result result query-m {:table? false}))
 
            ;; Default list behavior is to group result
            {}

+ 33 - 0
tldraw/apps/tldraw-logseq/src/components/ActionBar/ActionBar.tsx

@@ -6,6 +6,7 @@ import * as React from 'react'
 import type { Shape } from '../../lib'
 import { TablerIcon } from '../icons'
 import { Button } from '../Button'
+import { ToggleInput } from '../inputs/ToggleInput'
 import { ZoomMenu } from '../ZoomMenu'
 import * as Separator from '@radix-ui/react-separator'
 import { LogseqContext } from '../../lib/logseq-context'
@@ -32,6 +33,14 @@ export const ActionBar = observer(function ActionBar(): JSX.Element {
     app.api.zoomOut()
   }, [app])
 
+  const toggleGrid = React.useCallback(() => {
+    app.api.toggleGrid()
+  }, [app])
+
+  const toggleSnapToGrid = React.useCallback(() => {
+    app.api.toggleSnapToGrid()
+  }, [app])
+
   return (
     <div className="tl-action-bar" data-html2canvas-ignore="true">
       {!app.readOnly && (
@@ -55,6 +64,30 @@ export const ActionBar = observer(function ActionBar(): JSX.Element {
         <Separator.Root className="tl-toolbar-separator" orientation="vertical" />
         <ZoomMenu />
       </div>
+
+      <div className={'tl-toolbar tl-grid-bar ml-4'}>
+        <ToggleInput
+            tooltip={t('whiteboard/toggle-grid')}
+            className="tl-button"
+            pressed={app.settings.showGrid}
+            id="tl-show-grid"
+            onPressedChange={toggleGrid}
+          >
+          <TablerIcon name="grid-dots" />
+        </ToggleInput>
+
+        {!app.readOnly && (
+          <ToggleInput
+              tooltip={t('whiteboard/snap-to-grid')}
+              className="tl-button"
+              pressed={app.settings.snapToGrid}
+              id="tl-snap-to-grid"
+              onPressedChange={toggleSnapToGrid}
+            >
+            <TablerIcon name={app.settings.snapToGrid ? "magnet" : "magnet-off"} />
+          </ToggleInput>
+        )}
+      </div>
     </div>
   )
 })

+ 8 - 2
tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/CreatingState.tsx

@@ -1,4 +1,4 @@
-import { TLApp, TLTargetType, TLToolState, uniqueId } from '@tldraw/core'
+import { TLApp, TLTargetType, TLToolState, uniqueId, GRID_SIZE } from '@tldraw/core'
 import type { TLReactEventMap, TLReactEvents } from '@tldraw/react'
 import Vec from '@tldraw/vec'
 import { transaction } from 'mobx'
@@ -20,10 +20,16 @@ export class CreatingState extends TLToolState<
   onEnter = () => {
     this.app.history.pause()
     transaction(() => {
+      let point = Vec.sub(this.app.inputs.originPoint, this.offset)
+
+      if (this.app.settings.snapToGrid) {
+        point = Vec.snap(point, GRID_SIZE)
+      }
+
       const shape = new LogseqPortalShape({
         id: uniqueId(),
         parentId: this.app.currentPage.id,
-        point: Vec.sub(this.app.inputs.originPoint, this.offset),
+        point: point,
         size: LogseqPortalShape.defaultProps.size,
         fill: this.app.settings.color,
         stroke: this.app.settings.color,

+ 6 - 0
tldraw/packages/core/src/lib/TLApi/TLApi.ts

@@ -172,6 +172,12 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
     return this
   }
 
+  toggleSnapToGrid = (): this => {
+    const { settings } = this.app
+    settings.update({ snapToGrid: !settings.snapToGrid })
+    return this
+  }
+
   setColor = (color: string): this => {
     const { settings } = this.app
 

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

@@ -10,7 +10,6 @@ import type {
   TLCallback,
   TLEventMap,
   TLEvents,
-  TLShortcut,
   TLStateEvents,
   TLSubscription,
   TLSubscriptionEventInfo,

+ 3 - 6
tldraw/packages/core/src/lib/TLBaseLineBindingState.ts

@@ -7,7 +7,7 @@ import type { TLLineShape, TLLineShapeProps, TLShape } from './shapes'
 import type { TLApp } from './TLApp'
 import type { TLTool } from './TLTool'
 import { TLToolState } from './TLToolState'
-
+import { GRID_SIZE } from '@tldraw/core'
 export class TLBaseLineBindingState<
   S extends TLShape,
   T extends S & TLLineShape,
@@ -30,8 +30,7 @@ export class TLBaseLineBindingState<
   onPointerMove: TLStateEvents<S, K>['onPointerMove'] = () => {
     const {
       inputs: { shiftKey, previousPoint, originPoint, currentPoint, modKey, altKey },
-      settings: { showGrid },
-      currentGrid,
+      settings: { snapToGrid },
     } = this.app
     // @ts-expect-error just ignore
     const shape = this.app.getShapeById<TLLineShape>(this.initialShape.id)!
@@ -56,9 +55,7 @@ export class TLBaseLineBindingState<
     const handleChanges = {
       [handleId]: {
         ...handles[handleId],
-        // FIXME Snap not working properly
-        // point: showGrid ? Vec.snap(nextPoint, currentGrid) : Vec.toFixed(nextPoint)
-        point: Vec.toFixed(nextPoint),
+        point: snapToGrid ? Vec.snap(nextPoint, GRID_SIZE) : Vec.toFixed(nextPoint),
         bindingId: undefined,
       },
     }

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

@@ -4,6 +4,7 @@ import { observable, makeObservable, action } from 'mobx'
 export interface TLSettingsProps {
   mode: 'light' | 'dark'
   showGrid: boolean
+  snapToGrid: boolean
   color: string
   scaleLevel: string
 }
@@ -15,6 +16,7 @@ export class TLSettings implements TLSettingsProps {
 
   @observable mode: 'dark' | 'light' = 'light'
   @observable showGrid = true
+  @observable snapToGrid = true
   @observable scaleLevel = 'md'
   @observable color = ''
 

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

@@ -51,7 +51,7 @@ export class TLViewport {
   }
 
   panToPointWhenNearBounds = (point: number[]) => {
-    const threshold = [TLViewport.panThreshold, TLViewport.panThreshold]
+    const threshold =  Vec.div([TLViewport.panThreshold, TLViewport.panThreshold], this.camera.zoom)
 
     const deltaMax = Vec.sub([this.currentView.maxX, this.currentView.maxY], Vec.add(point, threshold))
     const deltaMin = Vec.sub([this.currentView.minX, this.currentView.minY], Vec.sub(point, threshold))
@@ -59,7 +59,7 @@ export class TLViewport {
     const deltaX = deltaMax[0] < 0 ? deltaMax[0] : deltaMin[0] > 0 ? deltaMin[0] : 0
     const deltaY = deltaMax[1] < 0 ? deltaMax[1] : deltaMin[1] > 0 ? deltaMin[1] : 0
 
-    this.panCamera(Vec.mul([deltaX, deltaY], -TLViewport.panMultiplier))
+    this.panCamera(Vec.mul([deltaX, deltaY], -TLViewport.panMultiplier * this.camera.zoom))
 }
 
 

+ 6 - 1
tldraw/packages/core/src/lib/tools/TLBoxTool/states/CreatingState.tsx

@@ -1,5 +1,6 @@
 import type { TLBoxTool } from '../TLBoxTool'
 import Vec from '@tldraw/vec'
+import { GRID_SIZE } from '@tldraw/core'
 import type { TLBounds } from '@tldraw/intersect'
 import { type TLEventMap, TLCursor, type TLStateEvents, TLResizeCorner } from '../../../../types'
 import { uniqueId, BoundsUtils } from '../../../../utils'
@@ -68,7 +69,7 @@ export class CreatingState<
     if (!this.creatingShape) throw Error('Expected a creating shape.')
     const { initialBounds } = this
     const { currentPoint, originPoint, shiftKey } = this.app.inputs
-    const bounds = BoundsUtils.getTransformedBoundingBox(
+    let bounds = BoundsUtils.getTransformedBoundingBox(
       initialBounds,
       TLResizeCorner.BottomRight,
       Vec.sub(currentPoint, originPoint),
@@ -78,6 +79,10 @@ export class CreatingState<
         !this.creatingShape.canChangeAspectRatio
     )
 
+    if (this.app.settings.snapToGrid) {
+      bounds = BoundsUtils.snapBoundsToGrid(bounds, GRID_SIZE)
+    }
+
     this.creatingShape.update({
       point: [bounds.minX, bounds.minY],
       size: [bounds.width, bounds.height],

+ 3 - 1
tldraw/packages/core/src/lib/tools/TLLineTool/states/CreatingState.tsx

@@ -5,6 +5,8 @@ import type { TLShape, TLLineShape } from '../../../shapes'
 import type { TLApp } from '../../../TLApp'
 import { TLBaseLineBindingState } from '../../../TLBaseLineBindingState'
 import type { TLLineTool } from '../TLLineTool'
+import Vec from '@tldraw/vec'
+import { GRID_SIZE } from '@tldraw/core'
 
 export class CreatingState<
   S extends TLShape,
@@ -31,7 +33,7 @@ export class CreatingState<
       id: uniqueId(),
       type: Shape.id,
       parentId: this.app.currentPage.id,
-      point: originPoint,
+      point: this.app.settings.snapToGrid ? Vec.snap(originPoint, GRID_SIZE) : originPoint,
       fill: this.app.settings.color,
       stroke: this.app.settings.color,
       scaleLevel: this.app.settings.scaleLevel,

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

@@ -1,4 +1,5 @@
 import type { TLBounds } from '@tldraw/intersect'
+import { GRID_SIZE } from '@tldraw/core'
 import { Vec } from '@tldraw/vec'
 import {
   type TLEventMap,
@@ -216,6 +217,11 @@ export class ResizingState<
       //   // Position the bounds at the center
       //   relativeBounds = BoundsUtils.centerBounds(relativeBounds, center)
       // }
+
+      if (this.app.settings.snapToGrid) {
+        relativeBounds = BoundsUtils.snapBoundsToGrid(relativeBounds, GRID_SIZE)
+      }
+
       shape.onResize(initialShapeProps, {
         center,
         rotation,

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

@@ -1,11 +1,12 @@
 import { Vec } from '@tldraw/vec'
 import { transaction } from 'mobx'
 import { type TLEventMap, TLCursor, type TLEvents } from '../../../../types'
-import { dedupe, uniqueId } from '../../../../utils'
+import { uniqueId } from '../../../../utils'
 import type { TLShape } from '../../../shapes'
 import type { TLApp } from '../../../TLApp'
 import { TLToolState } from '../../../TLToolState'
 import type { TLSelectTool } from '../TLSelectTool'
+import { GRID_SIZE } from '../../../../constants'
 
 export class TranslatingState<
   S extends TLShape,
@@ -46,9 +47,15 @@ export class TranslatingState<
     }
 
     transaction(() => {
-      this.app.allSelectedShapesArray.forEach(shape => {
-        if (!shape.props.isLocked) shape.update({ point: Vec.add(initialPoints[shape.id], delta) })
-      })
+      this.app.allSelectedShapesArray
+        .filter(s => !s.props.isLocked)
+        .forEach(shape => {
+          let position = Vec.add(initialPoints[shape.id], delta)
+          if (this.app.settings.snapToGrid) {
+            position = Vec.snap(position, GRID_SIZE)
+          }
+          shape.update({ point: position })
+        })
     })
   }
 

+ 4 - 1
tldraw/packages/core/src/lib/tools/TLTextTool/states/CreatingState.tsx

@@ -1,5 +1,6 @@
 import type { TLTextTool } from '../TLTextTool'
 import Vec from '@tldraw/vec'
+import { GRID_SIZE } from '@tldraw/core'
 import type { TLBounds } from '@tldraw/intersect'
 import { transaction } from 'mobx'
 import { type TLEventMap, TLCursor, TLTargetType } from '../../../../types'
@@ -43,8 +44,10 @@ export class CreatingState<
     this.creatingShape.setScaleLevel(this.app.settings.scaleLevel)
     transaction(() => {
       this.app.currentPage.addShapes(shape as unknown as S)
+      const point = this.app.settings.snapToGrid ? Vec.snap([...originPoint], GRID_SIZE) : originPoint
       const { bounds } = shape
-      shape.update({ point: Vec.sub(originPoint, [bounds.width / 2, bounds.height / 2]) })
+      shape.update({
+        point: Vec.sub(point, [bounds.width / 2, bounds.height / 2]) })
       this.app.transition('select')
       this.app.setSelectedShapes([shape as unknown as S])
       this.app.currentState.transition('editingShape', {