浏览代码

Merge branch 'master' into enhance/keymaps-manager-x

charlie 2 年之前
父节点
当前提交
cbeccfb9e9
共有 77 个文件被更改,包括 2133 次插入317 次删除
  1. 5 0
      .gitignore
  2. 2 2
      android/app/build.gradle
  3. 3 2
      deps.edn
  4. 4 0
      deps/graph-parser/.carve/ignore
  5. 0 1
      deps/graph-parser/src/logseq/graph_parser.cljs
  6. 10 23
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  7. 0 1
      deps/graph-parser/src/logseq/graph_parser/cli.cljs
  8. 1 7
      deps/graph-parser/src/logseq/graph_parser/config.cljs
  9. 6 1
      deps/graph-parser/src/logseq/graph_parser/property.cljs
  10. 1 1
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  11. 9 3
      deps/graph-parser/test/logseq/graph_parser_test.cljs
  12. 17 0
      deps/shui/.clj-kondo/config.edn
  13. 29 0
      deps/shui/README.md
  14. 1 0
      deps/shui/deps.edn
  15. 2 0
      deps/shui/shui-graph/journals/2023_03_27.md
  16. 348 0
      deps/shui/shui-graph/logseq/config.edn
  17. 0 0
      deps/shui/shui-graph/logseq/custom.css
  18. 22 0
      deps/shui/shui-graph/pages/About Shui.md
  19. 3 0
      deps/shui/shui-graph/pages/Page 1.md
  20. 1 0
      deps/shui/shui-graph/pages/Page 2.md
  21. 1 0
      deps/shui/shui-graph/pages/Page 3.md
  22. 4 0
      deps/shui/shui-graph/pages/contents.md
  23. 4 0
      deps/shui/shui-graph/pages/shui___components.md
  24. 62 0
      deps/shui/shui-graph/pages/shui___components___table.md
  25. 36 0
      deps/shui/src/logseq/shui/context.cljs
  26. 11 0
      deps/shui/src/logseq/shui/core.cljs
  27. 471 0
      deps/shui/src/logseq/shui/table/v2.cljs
  28. 81 0
      deps/shui/src/logseq/shui/util.cljs
  29. 15 2
      e2e-tests/basic.spec.ts
  30. 1 1
      e2e-tests/fixtures.ts
  31. 304 0
      e2e-tests/shui/table.spec.js
  32. 7 0
      e2e-tests/utils.ts
  33. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  34. 2 0
      package.json
  35. 1 0
      public/index.html
  36. 1 0
      resources/electron.html
  37. 1 0
      resources/index.html
  38. 10 0
      resources/js/tabler.ext.js
  39. 1 1
      resources/package.json
  40. 6 6
      shadow-cljs.edn
  41. 2 2
      src/main/frontend/commands.cljs
  42. 72 64
      src/main/frontend/components/block.cljs
  43. 2 1
      src/main/frontend/components/block.css
  44. 12 11
      src/main/frontend/components/page.cljs
  45. 1 0
      src/main/frontend/components/page.css
  46. 10 0
      src/main/frontend/components/plugins.css
  47. 4 3
      src/main/frontend/components/query.cljs
  48. 79 54
      src/main/frontend/components/query_table.cljs
  49. 1 1
      src/main/frontend/components/settings.cljs
  50. 6 1
      src/main/frontend/db/model.cljs
  51. 15 1
      src/main/frontend/dicts.cljc
  52. 0 2
      src/main/frontend/format/block.cljs
  53. 10 1
      src/main/frontend/fs/sync.cljs
  54. 0 2
      src/main/frontend/handler/common/file.cljs
  55. 9 8
      src/main/frontend/handler/editor.cljs
  56. 2 8
      src/main/frontend/handler/file_sync.cljs
  57. 10 7
      src/main/frontend/handler/whiteboard.cljs
  58. 12 4
      src/main/frontend/modules/outliner/core.cljs
  59. 7 0
      src/main/frontend/modules/outliner/datascript.cljc
  60. 6 6
      src/main/frontend/page.cljs
  61. 1 1
      src/main/frontend/rum.cljs
  62. 25 0
      src/main/frontend/shui.cljs
  63. 2 2
      src/main/frontend/state.cljs
  64. 24 24
      src/main/frontend/ui.cljs
  65. 1 1
      src/main/frontend/util/cursor.cljs
  66. 1 1
      src/main/frontend/version.cljs
  67. 4 20
      src/main/logseq/api.cljs
  68. 28 0
      src/main/logseq/api/block.cljs
  69. 3 2
      src/test/frontend/fs_test.cljs
  70. 2 1
      src/test/frontend/handler/plugin_config_test.cljs
  71. 1 1
      src/test/frontend/handler/repo_conversion_test.cljs
  72. 1 16
      src/test/frontend/test/helper.cljs
  73. 17 0
      src/test/frontend/test/node_helper.cljs
  74. 40 0
      src/test/logseq/api_test.cljs
  75. 226 16
      tailwind.config.js
  76. 0 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/TranslatingState.ts
  77. 10 0
      yarn.lock

+ 5 - 0
.gitignore

@@ -57,3 +57,8 @@ android/app/src/main/assets/capacitor.config.json
 /public/static
 .yarn/
 .yarnrc.yml
+
+deps/shui/.lsp
+deps/shui/.lsp-cache
+deps/shui/.clj-kondo
+deps/shui/shui-graph/logseq/bak

+ 2 - 2
android/app/build.gradle

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

+ 3 - 2
deps.edn

@@ -27,11 +27,12 @@
   camel-snake-kebab/camel-snake-kebab   {:mvn/version "0.4.2"}
   instaparse/instaparse                 {:mvn/version "1.4.10"}
   org.clojars.mmb90/cljs-cache          {:mvn/version "0.1.4"}
+  fipp/fipp                             {:mvn/version "0.6.26"}
   logseq/common                         {:local/root "deps/common"}
   logseq/graph-parser                   {:local/root "deps/graph-parser"}
   logseq/publishing                     {:local/root "deps/publishing"}
-  metosin/malli                         {:mvn/version "0.10.0"}
-  fipp/fipp                             {:mvn/version "0.6.26"}}
+  logseq/shui                           {:local/root "deps/shui"}
+  metosin/malli                         {:mvn/version "0.10.0"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}

+ 4 - 0
deps/graph-parser/.carve/ignore

@@ -46,3 +46,7 @@ logseq.graph-parser.text/get-file-basename
 logseq.graph-parser.mldoc/mldoc-link?
 ;; public var
 logseq.graph-parser.schema.mldoc/block-ast-coll-schema
+;; API
+logseq.graph-parser.config/img-formats
+;; API
+logseq.graph-parser.config/text-formats

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

@@ -89,7 +89,6 @@ Options available:
          {:keys [tx ast]}
          (let [extract-options' (merge {:block-pattern (gp-config/get-block-pattern format)
                                         :date-formatter "MMM do, yyyy"
-                                        :supported-formats (gp-config/supported-formats)
                                         :uri-encoded? false
                                         :filename-format :legacy}
                                        extract-options

+ 10 - 23
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -35,7 +35,7 @@
          (string/join))))
 
 (defn- get-page-reference
-  [block supported-formats]
+  [block]
   (let [page (cond
                (and (vector? block) (= "Link" (first block)))
                (let [typ (first (:url (second block)))
@@ -54,19 +54,6 @@
                    (page-ref/page-ref? value)
                    (text/page-ref-un-brackets! value))
 
-                  (and
-                   (= typ "Search")
-                   (not (contains? #{\# \* \/ \[} (first value)))
-                   ;; FIXME: use `gp-util/get-format` instead
-                   (let [ext (some-> (gp-util/get-file-ext value) keyword)]
-                     (when (and (not (string/starts-with? value "http:"))
-                                (not (string/starts-with? value "https:"))
-                                (not (string/starts-with? value "file:"))
-                                (not (gp-config/local-asset? value))
-                                (or (#{:excalidraw :tldr} ext)
-                                    (not (contains? supported-formats ext))))
-                       value)))
-
                   (and
                    (= typ "File")
                    (second (first (:label (second block)))))))
@@ -329,7 +316,7 @@
     nil))
 
 (defn- with-page-refs
-  [{:keys [title body tags refs marker priority] :as block} with-id? supported-formats db date-formatter]
+  [{:keys [title body tags refs marker priority] :as block} with-id? db date-formatter]
   (let [refs (->> (concat tags refs [marker priority])
                   (remove string/blank?)
                   (distinct))
@@ -340,7 +327,7 @@
        (when-not (and (vector? form)
                       (= (first form) "Custom")
                       (= (second form) "query"))
-         (when-let [page (get-page-reference form supported-formats)]
+         (when-let [page (get-page-reference form)]
            (swap! *refs conj page))
          (when-let [tag (get-tag form)]
            (let [tag (text/page-ref-un-brackets! tag)]
@@ -431,9 +418,9 @@
     (map (fn [page] (page-name->map page true db true date-formatter)) page-refs)))
 
 (defn- with-page-block-refs
-  [block with-id? supported-formats db date-formatter]
+  [block with-id? db date-formatter]
   (some-> block
-          (with-page-refs with-id? supported-formats db date-formatter)
+          (with-page-refs with-id? db date-formatter)
           with-block-refs
           block-tags->pages
           (update :refs (fn [col] (remove nil? col)))))
@@ -507,7 +494,7 @@
     (mapv macro->block @*result)))
 
 (defn with-pre-block-if-exists
-  [blocks body pre-block-properties encoded-content {:keys [supported-formats db date-formatter user-config]}]
+  [blocks body pre-block-properties encoded-content {:keys [db date-formatter user-config]}]
   (let [first-block (first blocks)
         first-block-start-pos (get-in first-block [:block/meta :start_pos])
 
@@ -539,7 +526,7 @@
                                 :block/macros (extract-macros-from-ast body)
                                 :block/body body}
                          {:keys [tags refs]}
-                         (with-page-block-refs {:body body :refs property-refs} false supported-formats db date-formatter)]
+                         (with-page-block-refs {:body body :refs property-refs} false db date-formatter)]
                      (cond-> block
                              tags
                              (assoc :block/tags tags)
@@ -557,7 +544,7 @@
     properties))
 
 (defn- construct-block
-  [block properties timestamps body encoded-content format pos-meta with-id? {:keys [block-pattern supported-formats db date-formatter]}]
+  [block properties timestamps body encoded-content format pos-meta with-id? {:keys [block-pattern db date-formatter]}]
   (let [id (get-custom-id-or-new-id properties)
         ref-pages-in-properties (->> (:page-refs properties)
                                      (remove string/blank?))
@@ -594,7 +581,7 @@
                 (merge block (timestamps->scheduled-and-deadline timestamps))
                 block)
         block (assoc block :body body)
-        block (with-page-block-refs block with-id? supported-formats db date-formatter)
+        block (with-page-block-refs block with-id? db date-formatter)
         block (update block :refs concat (:block-refs properties))
         {:keys [created-at updated-at]} (:properties properties)
         block (cond-> block
@@ -648,7 +635,7 @@
     `content`: markdown or org-mode text.
     `with-id?`: If `with-id?` equals to true, all the referenced pages will have new db ids.
     `format`: content's format, it could be either :markdown or :org-mode.
-    `options`: Options supported are :user-config, :block-pattern :supported-formats,
+    `options`: Options supported are :user-config, :block-pattern,
                :extract-macros, :date-formatter, :page-name and :db"
   [blocks content with-id? format {:keys [user-config] :as options}]
   {:pre [(seq blocks) (string? content) (boolean? with-id?) (contains? #{:markdown :org} format)]}

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

@@ -48,7 +48,6 @@ TODO: Fail fast when process exits 1"
   [conn files {:keys [config] :as options}]
   (let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config)
                                 :user-config config
-                                :supported-formats (gp-config/supported-formats)
                                 :filename-format (or (:file/name-format config) :legacy)
                                 :extracted-block-ids (atom #{})}
                                (select-keys options [:verbose]))]

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

@@ -1,7 +1,6 @@
 (ns logseq.graph-parser.config
   "App config that is shared between graph-parser and rest of app"
-  (:require [clojure.set :as set]
-            [clojure.string :as string]
+  (:require [clojure.string :as string]
             [goog.object :as gobj]))
 
 (def app-name
@@ -68,11 +67,6 @@
   []
   #{:gif :svg :jpeg :ico :png :jpg :bmp :webp})
 
-(defn supported-formats
-  []
-  (set/union (text-formats)
-             (img-formats)))
-
 (defn get-date-formatter
   [config]
   (or

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

@@ -51,7 +51,12 @@
   "Properties used by logseq that user can edit"
   []
   (into #{:title :icon :template :template-including-parent :public :filters :exclude-from-graph-view
-          :logseq.query/nlp-date
+          :logseq.query/nlp-date 
+          ;; view props 
+          :logseq.color
+          ;; table props
+          :logseq.table.version :logseq.table.compact :logseq.table.headers :logseq.table.hover 
+          :logseq.table.borders :logseq.table.stripes :logseq.table.max-width
           ;; org-mode only
           :macro :filetags}
         editable-linkable-built-in-properties))

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

@@ -153,7 +153,7 @@
   ;; only increase over time as the docs graph rarely has deletions
   (testing "Counts"
     (is (= 306 (count files)) "Correct file count")
-    (is (= 69508 (count (d/datoms db :eavt))) "Correct datoms count")
+    (is (= 69500 (count (d/datoms db :eavt))) "Correct datoms count")
 
     (is (= 5866
            (ffirst

+ 9 - 3
deps/graph-parser/test/logseq/graph_parser_test.cljs

@@ -360,15 +360,21 @@
                   (remove built-in-pages)
                   set)))))
 
-  (testing "for file and web uris"
+  (testing "for file, mailto, web and other uris"
     (let [conn (ldb/start-conn)
           built-in-pages (set (map string/lower-case default-db/built-in-pages-names))]
       (graph-parser/parse-file conn
                                "foo.md"
-                               (str "- [Filename.txt](file:///E:/test/Filename.txt)\n"
+                               (str "- [foo]([[bar]])\n"
+                                    ;; all of the uris below do not create pages
+                                    "- ![image.png](../assets/image_1630480711363_0.png)\n"
+                                    "- [Filename.txt](file:///E:/test/Filename.txt)\n"
+                                    "- [mail](mailto:[email protected]?subject=TestSubject)\n"
+                                    "- [onenote link](onenote:https://d.docs.live.net/b2127346582e6386a/blablabla/blablabla/blablabla%20blablabla.one#Etat%202019&section-id={133DDF16-9A1F-4815-9A05-44303784442E6F94}&page-id={3AAB677F0B-328F-41D0-AFF5-66408819C085}&end)\n"
+                                    "- [lock file](deps/graph-parser/yarn.lock)"
                                     "- [example](https://example.com)")
                                {})
-      (is (= #{"foo"}
+      (is (= #{"foo" "bar"}
              (->> (d/q '[:find (pull ?b [*])
                          :in $
                          :where [?b :block/name]]

+ 17 - 0
deps/shui/.clj-kondo/config.edn

@@ -0,0 +1,17 @@
+{ :config-in-ns
+ ;; :used-underscored-binding is turned off for components because of false positive
+ ;; for rum/defcs and _state.
+ {all-components {:linters {:used-underscored-binding {:level :off}}}}
+
+ :linters 
+ {;; Disable until it doesn't trigger false positives on rum/defcontext
+  :earmuffed-var-not-dynamic {:level :off}}
+ :hooks {:analyze-call {rum.core/defc hooks.rum/defc
+                        rum.core/defcs hooks.rum/defcs
+                        clojure.string/join hooks.path-invalid-construct/string-join}}
+ :lint-as {rum.core/defcc rum.core/defc
+           rum.core/with-context clojure.core/let
+           rum.core/defcontext clojure.core/def
+           rum.core/defc clojure.core/defn
+           rum.core/defcs clojure.core/defn
+           frontend.react/defc clojure.core/defn}}

+ 29 - 0
deps/shui/README.md

@@ -0,0 +1,29 @@
+## Description
+
+This library provides a set of UI components for use within logseq.
+
+## API
+
+This library is under the parent namespace `logseq.shui`. This library provides 
+several namespaces, all of which will be versioned, with the exception of `logseq.shui.context`.
+
+An example of a versioned namespace is the table namespace:
+
+`logseq.shui.table.v2`
+
+`root` components are exported from each versioned file to indicate the root component to be rendered:
+
+`logseq.shui.table.v2/root`
+
+Each root component should expect two arguments, `props` and `context`. 
+
+## `props`
+
+Ultimately, components in shui will need to be used by JavaScript. While it is idiomatic in clojure to
+use a list of properties, it is idiomatic in react to use a single props map. Shui components should therefore 
+stick to this convention when possible to ease the conversion between the two languages. 
+
+## `context` 
+
+Context is a set of functions that call back to the main application. These are abstracted out into a context 
+object to make it clear what is used internally, and what is used externally.

+ 1 - 0
deps/shui/deps.edn

@@ -0,0 +1 @@
+{:paths ["src"]}

+ 2 - 0
deps/shui/shui-graph/journals/2023_03_27.md

@@ -0,0 +1,2 @@
+-
+-

+ 348 - 0
deps/shui/shui-graph/logseq/config.edn

@@ -0,0 +1,348 @@
+{:meta/version 1
+
+ ;; Currently, we support either "Markdown" or "Org".
+ ;; This can overwrite your global preference so that
+ ;; maybe your personal preferred format is Org but you'd
+ ;; need to use Markdown for some projects.
+ ;; :preferred-format ""
+
+ ;; Preferred workflow style.
+ ;; Value is either ":now" for NOW/LATER style,
+ ;; or ":todo" for TODO/DOING style.
+ :preferred-workflow :now
+
+ ;; The app will ignore those directories or files.
+ ;; E.g. :hidden ["/archived" "/test.md" "../assets/archived"]
+ :hidden []
+
+ ;; When creating the new journal page, the app will use your template if there is one.
+ ;; You only need to input your template name here.
+ :default-templates
+ {:journals ""}
+
+ ;; Set a custom date format for journal page title
+ ;; Example:
+ ;; :journal/page-title-format "EEE, do MMM yyyy"
+
+ ;; Whether to enable hover on tooltip preview feature
+ ;; Default is true, you can also toggle this via setting page
+ :ui/enable-tooltip? true
+
+ ;; Show brackets around page references
+ ;; :ui/show-brackets? true
+
+ ;; Enable showing the body of blocks when referencing them.
+ :ui/show-full-blocks? false
+
+ ;; Enable Block timestamp
+ :feature/enable-block-timestamps? false
+
+ ;; Enable remove accents when searching.
+ ;; After toggle this option, please remember to rebuild your search index by press (cmd+c cmd+s).
+ :feature/enable-search-remove-accents? true
+
+ ;; Enable journals
+ ;; :feature/enable-journals? true
+
+ ;; Enable flashcards
+ ;; :feature/enable-flashcards? true
+
+ ;; Enable Whiteboards
+ ;; :feature/enable-whiteboards? true
+
+ ;; Disable the built-in Scheduled tasks and deadlines query
+ ;; :feature/disable-scheduled-and-deadline-query? true
+
+ ;; Specify the number of days in the future to display in the
+ ;; scheduled tasks and deadlines query, with a default value of 0 which
+ ;; only displays tasks for today.
+ ;; Example usage:
+ ;; Display all scheduled tasks and deadlines in the next 7 days
+ ;; :scheduled/future-days 7
+
+ ;; Specify the date on which the week starts.
+ ;; Goes from 0 to 6 (Monday to Sunday), default to 6
+ :start-of-week 6
+
+ ;; Specify a custom CSS import
+ ;; This option take precedence over your local `logseq/custom.css` file
+ ;; You may find a list of awesome logseq themes here:
+ ;; https://github.com/logseq/awesome-logseq#css-themes
+ ;; Example:
+ ;; :custom-css-url "@import url('https://cdn.jsdelivr.net/gh/dracula/logseq@master/custom.css');"
+
+ ;; Specify a custom js import
+ ;; This option take precedence over your local `logseq/custom.js` file
+ ;; :custom-js-url ""
+
+ ;; Set a custom Arweave gateway
+ ;; Default gateway: https://arweave.net
+ ;; :arweave/gateway ""
+
+ ;; Set Bullet indentation when exporting
+ ;; default option: tab
+ ;; Possible options are for `:sidebar` are
+ ;; 1. `:eight-spaces` as eight spaces
+ ;; 2. `:four-spaces` as four spaces
+ ;; 3. `:two-spaces` as two spaces
+ ;; :export/bullet-indentation :tab
+
+ ;; When :all-pages-public? true, export repo would export all pages within that repo.
+ ;; Regardless of whether you've set any page to public or not.
+ ;; Example:
+ ;; :publishing/all-pages-public? true
+
+ ;; Specify default home page and sidebar status for Logseq
+ ;; If not specified, Logseq default opens journals page on startup
+ ;; value for `:page` is name of page
+ ;; Possible options for `:sidebar` are
+ ;; 1. `"Contents"` to open up `Contents` in sidebar by default
+ ;; 2. `page name` to open up some page in sidebar
+ ;; 3. Or multiple pages in an array ["Contents" "Page A" "Page B"]
+ ;; If `:sidebar` is not set, sidebar will be hidden
+ ;; Example:
+ ;; 1. Setup page "Changelog" as home page and "Contents" in sidebar
+ ;; :default-home {:page "Changelog", :sidebar "Contents"}
+ ;; 2. Setup page "Jun 3rd, 2021" as home page without sidebar
+ ;; :default-home {:page "Jun 3rd, 2021"}
+ ;; 3. Setup page "home" as home page with multiple pages in sidebar
+ ;; :default-home {:page "home" :sidebar ["page a" "page b"]}
+ :default-home {:page "Contents"}
+
+ ;; Tell logseq to use a specific folder in the repo as a default location for notes
+ ;; if not specified, notes are stored in `pages` directory
+ ;; :pages-directory "your-directory"
+
+ ;; Tell logseq to use a specific folder in the repo as a default location for journals
+ ;; if not specified, journals are stored in `journals` directory
+ ;; :journals-directory "your-directory"
+
+ ;; Set this to true will convert
+ ;; `[[Grant Ideas]]` to `[[file:./grant_ideas.org][Grant Ideas]]` for org-mode
+ ;; For more, see https://github.com/logseq/logseq/issues/672
+ ;; :org-mode/insert-file-link? true
+
+ ;; Setup custom shortcuts under `:shortcuts` key
+ ;; Syntax:
+ ;; 1. `+` means keys pressing simultaneously. eg: `ctrl+shift+a`
+ ;; 2. ` ` empty space between keys represents key chords. eg: `t s` means press `t` followed by `s`
+ ;; 3. `mod` means `Ctrl` for Windows/Linux  and `Command` for Mac
+ ;; 4. use `false` to disable particular shortcut
+ ;; 5. you can define multiple bindings for one action, eg `["ctrl+j" "down"]`
+ ;; full list of configurable shortcuts are available below:
+ ;; https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/config.cljs
+ ;; Example:
+ ;; :shortcuts
+ ;; {:editor/new-block       "enter"
+ ;;  :editor/new-line        "shift+enter"
+ ;;  :editor/insert-link     "mod+shift+k"
+ ;;  :editor/highlight       false
+ ;;  :ui/toggle-settings     "t s"
+ ;;  :editor/up              ["ctrl+k" "up"]
+ ;;  :editor/down            ["ctrl+j" "down"]
+ ;;  :editor/left            ["ctrl+h" "left"]
+ ;;  :editor/right           ["ctrl+l" "right"]}
+ :shortcuts {}
+
+ ;; By default, pressing `Enter` in the document mode will create a new line.
+ ;; Set this to `true` so that it's the same behaviour as the usual outliner mode.
+ :shortcut/doc-mode-enter-for-new-block? false
+
+ ;; Block content larger than `block/content-max-length` will not be searchable
+ ;; or editable for performance.
+ :block/content-max-length 10000
+
+ ;; Whether to show command doc on hover
+ :ui/show-command-doc? true
+
+ ;; Whether to show empty bullets for non-document mode (the default mode)
+ :ui/show-empty-bullets? false
+
+ ;; Pre-defined :view function to use with advanced queries
+ :query/views
+ {:pprint
+  (fn [r] [:pre.code (pprint r)])}
+
+ ;; Pre-defined :result-transform function for use with advanced queries
+ :query/result-transforms
+ {:sort-by-priority
+  (fn [result] (sort-by (fn [h] (get h :block/priority "Z")) result))}
+
+ ;; The app will show those queries in today's journal page,
+ ;; the "NOW" query asks the tasks which need to be finished "now",
+ ;; the "NEXT" query asks the future tasks.
+ :default-queries
+ {:journals
+  [{:title "🔨 NOW"
+    :query [:find (pull ?h [*])
+            :in $ ?start ?today
+            :where
+            [?h :block/marker ?marker]
+            [(contains? #{"NOW" "DOING"} ?marker)]
+            [?h :block/page ?p]
+            [?p :block/journal? true]
+            [?p :block/journal-day ?d]
+            [(>= ?d ?start)]
+            [(<= ?d ?today)]]
+    :inputs [:14d :today]
+    :result-transform (fn [result]
+                        (sort-by (fn [h]
+                                   (get h :block/priority "Z")) result))
+    :collapsed? false}
+   {:title "📅 NEXT"
+    :query [:find (pull ?h [*])
+            :in $ ?start ?next
+            :where
+            [?h :block/marker ?marker]
+            [(contains? #{"NOW" "LATER" "TODO"} ?marker)]
+            [?h :block/page ?p]
+            [?p :block/journal? true]
+            [?p :block/journal-day ?d]
+            [(> ?d ?start)]
+            [(< ?d ?next)]]
+    :inputs [:today :7d-after]
+    :collapsed? false}]}
+
+ ;; Add your own commands to slash menu to speedup.
+ ;; E.g.
+ ;; :commands
+ ;; [
+ ;; ["js" "Javascript"]
+ ;; ["md" "Markdown"]
+ ;; ]
+ :commands
+ []
+
+ ;; By default, a block can only be collapsed if it has some children.
+ ;; `:outliner/block-title-collapse-enabled? true` enables a block with a title
+ ;; (multiple lines) can be collapsed too. For example:
+ ;; - block title
+ ;;   block content
+ :outliner/block-title-collapse-enabled? false
+
+ ;; Macros replace texts and will make you more productive.
+ ;; For example:
+ ;; Change the :macros value below to:
+ ;; {"poem" "Rose is $1, violet's $2. Life's ordered: Org assists you."}
+ ;; input "{{poem red,blue}}"
+ ;; becomes
+ ;; Rose is red, violet's blue. Life's ordered: Org assists you.
+ :macros {}
+
+ ;; The default level to be opened for the linked references.
+ ;; For example, if we have some example blocks like this:
+ ;; - a [[page]] (level 1)
+ ;;   - b        (level 2)
+ ;;     - c      (level 3)
+ ;;       - d    (level 4)
+ ;;
+ ;; With the default value of level 2, `b` will be collapsed.
+ ;; If we set the level's value to 3, `b` will be opened and `c` will be collapsed.
+ :ref/default-open-blocks-level 2
+
+ :ref/linked-references-collapsed-threshold 50
+
+ ;; Favorites to list on the left sidebar
+ :favorites []
+
+ ;; any number between 0 and 1 (the greater it is the faster the changes of the next-interval of card reviews) (default 0.5)
+ ;; :srs/learning-fraction 0.5
+
+ ;; the initial interval after the first successful review of a card (default 4)
+ ;; :srs/initial-interval 4
+
+ ;; hide specific properties for blocks
+ ;; E.g. :block-hidden-properties #{:created-at :updated-at}
+ ;; :block-hidden-properties #{}
+
+ ;; Enable all your properties to have corresponding pages
+ :property-pages/enabled? true
+
+ ;; Properties to exclude from having property pages
+ ;; E.g.:property-pages/excludelist #{:duration :author}
+ ;; :property-pages/excludelist
+
+ ;; By default, property value separated by commas will not be treated as
+ ;; page references. You can add properties to enable it.
+ ;; E.g. :property/separated-by-commas #{:alias :tags}
+ ;; :property/separated-by-commas #{}
+
+ ;; Properties that are ignored when parsing property values for references
+ ;; :ignored-page-references-keywords #{"author" "startup"}
+
+ ;; logbook setup
+ ;; :logbook/settings
+ ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated
+ ;;  :enabled-in-all-blocks true ;display logbook in all blocks after timetracking
+ ;;  :enabled-in-timestamped-blocks false ;don't display logbook at all
+ ;; }
+
+ ;; Mobile photo uploading setup
+ ;; :mobile/photo
+ ;; {:allow-editing? true
+ ;;  :quality          80}
+
+ ;; Mobile features options
+ ;; Gestures
+ ;; :mobile
+ ;; {:gestures/disabled-in-block-with-tags ["kanban"]}
+
+ ;; Extra CodeMirror options
+ ;; See https://codemirror.net/5/doc/manual.html#config for possible options
+ ;; :editor/extra-codemirror-options {:keyMap "emacs" :lineWrapping true}
+
+ ;; Enable logical outdenting
+ ;; :editor/logical-outdenting? true
+
+ ;; When both text and a file are in the clipboard, paste the file
+ ;; :editor/preferred-pasting-file? true
+
+ ;; Quick capture templates for receiving contents from other apps.
+ ;; Each template contains three elements {time}, {text} and {url}, which can be auto-expanded
+ ;; by received contents from other apps. Note: the {} cannot be omitted.
+ ;; - {time}: capture time
+ ;; - {date}: capture date using current date format, use `[[{date}]]` to get a page reference
+ ;; - {text}: text that users selected before sharing.
+ ;; - {url}: url or assets path for media files stored in Logseq.
+ ;; You can also reorder them, or even only use one or two of them in the template.
+ ;; You can also insert or format any text in the template as shown in the following examples.
+ ;; :quick-capture-templates
+ ;; {:text "[[quick capture]] **{time}**: {text} from {url}"
+ ;;  :media "[[quick capture]] **{time}**: {url}"}
+
+ ;; Quick capture options
+ ;; :quick-capture-options {:insert-today? false :redirect-page? false :default-page nil}
+
+ ;; File sync options
+ ;; Ignore these files when syncing, regexp is supported.
+ ;; :file-sync/ignore-files []
+
+ ;; dwim (do what I mean) for Enter key when editing.
+ ;; Context-awareness of Enter key makes editing more easily
+ ; :dwim/settings {
+ ;   :admonition&src?  true
+ ;   :markup?          false
+ ;   :block-ref?       true
+ ;   :page-ref?        true
+ ;   :properties?      true
+ ;   :list?            true
+ ; }
+
+ ;; Decide the way to escape the special characters in the page title.
+ ;; Warning:
+ ;;   This is a dangerous operation. If you want to change the setting,
+ ;;   should access the setting `Filename format` and follow the instructions.
+ ;;   Or you have to rename all the affected files manually then re-index on all
+ ;;   clients after the files are synced. Wrong handling may cause page titles
+ ;;   containing special characters to be messy.
+ ;; Available values:
+ ;;   :file/name-format :triple-lowbar
+ ;;     ;use triple underscore `___` for slash `/` in page title
+ ;;     ;use Percent-encoding for other invalid characters
+ :file/name-format :triple-lowbar
+ :feature/enable-whiteboards? true}
+
+ ;; specify the format of the filename for journal files
+ ;; :journal/file-name-format "yyyy_MM_dd"
+
+ 

+ 0 - 0
deps/shui/shui-graph/logseq/custom.css


+ 22 - 0
deps/shui/shui-graph/pages/About Shui.md

@@ -0,0 +1,22 @@
+- ## What is shui?
+- Shui is the component library for logseq. It has 3 main goals:
+	- 1. Provide an abstraction for specific components, separate from the main codebase
+	  2. Provide a consistent look and feel for the future of logseq
+	  3. Provide ready to use components to plugin authors to allow for a more consistent better user experience of plugin authors and users
+-
+- ## What are the general concepts of shui?
+- Shui has a few core principles:
+	- ### Focus on a native core experience
+		- We want to provide a smooth, consistent, and native feel for all logseq features, first and foremost
+	- ### Specific output, general input
+		- Components should be generally reusable by their props, however should have the user experience themselves
+		- Eventually, getting to a highly composable components is a great goal, but we should start small and focused first
+	- ### UI is a marathon, not a sprint
+		- Components in shui should be versioned, and should expect to evolve over time
+		- We need to go from highly coupled, low reused components to a loosely coupled, highly reusable library. This will take time, and means components have to be adaptable over time
+		- Versioning is at the core of shui
+-
+- ## How to contribute to shui?
+- In the logseq repo, there is a directory at `deps/shui`. Here you can find all of the shui components
+- In the logseq repo, you can find a copy of this graph at `deps/shui/shui-graph`. Here you can find and add all the test cases needed for different `shui` components
+- In the logseq repo, you can find tests under the `e2e-tests/shui`. To keep our infra streamlined, `shui` is bundled with and tested with the current CI for logseq

+ 3 - 0
deps/shui/shui-graph/pages/Page 1.md

@@ -0,0 +1,3 @@
+table-example:: true
+
+-

+ 1 - 0
deps/shui/shui-graph/pages/Page 2.md

@@ -0,0 +1 @@
+table-example:: true

+ 1 - 0
deps/shui/shui-graph/pages/Page 3.md

@@ -0,0 +1 @@
+table-example:: true

+ 4 - 0
deps/shui/shui-graph/pages/contents.md

@@ -0,0 +1,4 @@
+- [[About Shui]]
+- [[shui/components]]
+	- [[shui/components/table]]
+	-

+ 4 - 0
deps/shui/shui-graph/pages/shui___components.md

@@ -0,0 +1,4 @@
+- Below is a list of components that can be found in the shui library
+- [[shui/components/table]]
+	- The table component is used to render tabular data.
+-

+ 62 - 0
deps/shui/shui-graph/pages/shui___components___table.md

@@ -0,0 +1,62 @@
+- ### Props
+	- logseq.table.version:: 2
+	  logseq.table.hover:: row
+	  logseq.table.stripes:: true
+	  logseq.table.borders:: false
+	  | Prop Name | Description | Values |
+	  | --- | --- | --- |
+	  | `logseq.table.version` | The version of the table | 1, 2 |
+	  | `logseq.table.hover` | The hover effect of the table | cell (default), row, col, both, none |
+	  | `logseq.table.compact` | Whether to show a compact version of the data | false (default), true |
+	  | `logseq.table.headers` | The casing that should be applied to the header cols | none (default), uppercase, capitalize, capitalize-first, lowercase |
+	  | `logseq.table.borders` | Whether or not the table should have borders between all cells and rows | true (default), false |
+	  | `logseq.table.stripes` | Whether or not the table should have alternately colored table rows | false (default), true |
+	  | `logseq.table.max-width` | The maximum width (in rems) that should be applied to each column | <any number> (default 30) |
+	  | `logseq.color` | The color accent of the table | red, orange, yellow, green, blue, purple |
+- ### Examples
+	- #### Simplest possible markdown table
+	  collapsed:: true
+		- logseq.table.version:: 1
+		  | Fruit | Color |
+		  | Apples | Red |
+		  | Bananas | Yellow |
+	- #### Longer more complicated markdown table, with various widths and input types
+	  collapsed:: true
+		- logseq.table.version:: 2
+		  | Length | Text | EN | ZH |
+		  | --- | --- | --- | --- |
+		  | 70 | Logseq is a new note-taking app that has been making waves in the productivity community. | x |  |
+		  | 138 | With its unique approach to linking and organizing information, Logseq allows users to create a highly interconnected and personalized knowledge base. | x |  |
+		  | 194 | Unlike traditional note-taking apps, Logseq encourages users to embrace the power of plain text and markdown formatting, enabling them to easily manipulate and query their notes. | x |  |
+		  | 246 | From students to researchers, Logseq's flexible and intuitive interface makes it an ideal tool for anyone looking to optimize their note-taking and knowledge management workflow. | x |  |
+		  | 312 | Whether you're looking to organize your thoughts, collaborate with others, or simply streamline your note-taking process, Logseq offers a revolutionary approach that is sure to revolutionize the way you work. | x |  |
+		  | 35 | Logseq 是一款在生产力社群中备受瞩目的新型笔记应用。|  | x |
+		  | 59 | Logseq 以其独特的链接和组织信息方式,使用户能够创建高度互联且个性化的知识库。 |  | x | 86 | 不同于传统笔记应用,Logseq 鼓励用户采用纯文本和 Markdown 格式,使其能够轻松地操作和查询笔记。 |  | x |
+		  | 123 | 从学生到研究人员,Logseq 灵活直观的界面使其成为任何想要优化笔记和知识管理工作流程的人的理想工具。|  | x |
+		  | 152 | 无论您是想整理自己的思路、与他人合作,还是简化笔记流程,Logseq 提供的革命性方法肯定会改变您的工作方式。|  | x |
+	- #### Query table for blocks
+		- logseq.table.version:: 2
+		  query-table:: true
+		  query-properties:: [:block]
+		  logseq.table.borders:: false
+		  {{query #table-example/block}}
+		-
+		- #### data
+			- Block 1 #table-example/block
+			  table-example:: true
+			- Block 2 #table-example/block
+			  table-example:: true
+			- Block 3 #table-example/block
+			  table-example:: true
+	- #### Query table for pages
+		- {{query (page-property "table-example" "true")}}
+		  logseq.table.version:: 2
+		- [[Page 1]]
+		- [[Page 2]]
+		- [[Page 3]]
+	- #### Query table for mixed pages and blocks
+		- {{query (property "table-example" true)}}
+		  query-table:: true
+		  logseq.table.version:: 2
+	-
+- {{query }}

+ 36 - 0
deps/shui/src/logseq/shui/context.cljs

@@ -0,0 +1,36 @@
+(ns logseq.shui.context)
+
+(defn inline->inline-block [inline block-config]
+  (fn [_context item]
+    (inline block-config item)))
+
+(defn inline->map-inline-block [inline block-config]
+  (let [inline* (inline->inline-block inline block-config)]
+    (fn [context col]
+      (map #(inline* context %) col))))
+
+(defn make-context [{:keys [block-config app-config inline int->local-time-2]}]
+  {;; Shui needs access to the global configuration of the application
+   :config app-config
+   ;; Until components are converted over, they need to fallback to the old inline function 
+   ;; Wrap the old inline function to allow for interception, but fallback to the old inline function
+   :inline-block (inline->inline-block inline block-config) 
+   :map-inline-block (inline->map-inline-block inline block-config)
+   ;; Currently frontend component are provided an object map containin at least the following keys:
+   ;; These will be passed through in a whitelisted fashion so as to be able to track the dependencies  
+   ;; back to the core application
+   ;; TODO: document the following
+   :block (:block block-config)  ;; the db entity of the current block
+   :block? (:block? block-config)
+   :blocks-container-id (:blocks-container-id block-config)
+   :editor-box (:editor-box block-config)
+   :id (:id block-config) 
+   :mode? (:mode? block-config)
+   :query-result (:query-result block-config)
+   :sidebar? (:sidebar? block-config)
+   :start-time (:start-time block-config)
+   :uuid (:uuid block-config)
+   :whiteboard? (:whiteboard? block-config)
+   ;; Some functions from logseq's application will be used in the shui components. To avoid circular dependencies,
+   ;; they will be provided via the context object
+   :int->local-time-2 int->local-time-2})

+ 11 - 0
deps/shui/src/logseq/shui/core.cljs

@@ -0,0 +1,11 @@
+(ns logseq.shui.core
+  (:require 
+    [logseq.shui.table.v2 :as shui.table.v2]
+    [logseq.shui.context :as shui.context]))
+
+;; table component
+(def table shui.table.v2/root)
+(def table-v2 shui.table.v2/root)
+
+;; context
+(def make-context shui.context/make-context)

+ 471 - 0
deps/shui/src/logseq/shui/table/v2.cljs

@@ -0,0 +1,471 @@
+(ns logseq.shui.table.v2
+  (:require 
+    [clojure.string :as str]
+    [logseq.shui.util :refer [use-ref-bounding-client-rect use-dom-bounding-client-rect $main-content] :as util]
+    [rum.core :as rum]))
+
+(declare table-cell)
+
+(def COLORS #{"tomato" "red" "crimson" "pink" "plum" "purple" "violet" "indigo" "blue" "sky" "cyan" "teal" "mint" "green" "grass" "lime" "yellow" "amber" "orange" "brown"})
+(def MAX_WIDTH 30 #_rem) ;; Max width in rem for a single column
+(def MIN_WIDTH 4 #_rem)  ;; Min width in rem for a single column
+
+;; in order to make sure the tailwind classes are included,
+;; the values are pulled from the classes via regex.
+;; the return values are simply the numbers in the classes.
+(def CELL_PADDING         (->> "px-[0.75rem]" (re-find #"\d+\.?\d*") js/parseFloat))
+(def CELL_PADDING_COMPACT (->> "px-[0.25rem]" (re-find #"\d+\.?\d*") js/parseFloat))
+(def BORDER_WIDTH         (->> "border-[1px]" (re-find #"\d+\.?\d*") js/parseFloat))
+
+;; -- Helpers ------------------------------------------------------------------
+
+(defn get-in-first 
+  ([obj path] (get-in obj path))
+  ([obj path & more] (get-in obj path (apply get-in-first obj more))))
+
+(defn get-in-first-fallback
+  ([obj path] (get-in obj path))
+  ([obj path fallback] (get-in obj path fallback))
+  ([obj path path-b & more] (get-in obj path (apply get-in-first-fallback obj path-b more))))
+
+(defn read-prop [value]
+  (case value 
+    "false" false 
+    "true" true 
+    value))
+
+(defn get-view-prop 
+  "Get the config for a specified item. Can be overridden in blocks, specified in config, 
+  fallback to default config, or fallback to the provided parameters"
+  ([context kw] 
+   (read-prop
+     (get-in-first context [:block :properties kw] 
+                           [:block :block/properties kw] 
+                           [:config kw])))
+  ([context kw fallback]
+   (read-prop
+     (get-in-first-fallback context [:block :properties kw] 
+                                    [:block :block/properties kw] 
+                                    [:config kw] 
+                                    fallback))))
+
+(defn color->gray [color]
+  (case color 
+    ("tomato" "red" "crimson" "pink" "plum" "purple" "violet") "mauve"
+    ("indigo" "blue" "sky" "cyan") "slate" 
+    ("teal" "mint" "green") "sage"
+    ("grass" "lime") "olive"
+    ("yellow" "amber" "orange" "brown") "sand"
+    nil))
+
+(defn rdx
+  ([color step] (str "bg-" color "-" step))
+  ([param color step] (str (name param) "-" color "-" step)))
+  ; ([color step] (str "bg-" color "dark-" step))
+  ; ([param color step] (str param "-" color "dark-" step))))
+
+    ; --ls-primary-background-color: #fff;
+    ; --ls-secondary-background-color: #f8f8f8;
+    ; --ls-tertiary-background-color: #f2f2f3;
+    ; --ls-quaternary-background-color: #ebeaea));
+
+(defn lsx 
+  "This is a temporary bridge between the radix color grading and the
+  current logseq theming variables. Should set the prop to the given css variable"
+  ([step] (lsx :bg step))
+  ([param step]
+   (case step 
+     1 ({"bg" "bg-[color:var(--ls-primary-background-color)]"} (name param))
+     2 ({"bg" "bg-[color:var(--ls-secondary-background-color)]"} (name param)) 
+     3 ({"bg" "bg-[color:var(--ls-tertiary-background-color)]"} (name param))
+     4 ({"bg" "bg-[color:var(--ls-quaternary-background-color)]"} (name param))
+     5 ({"bg" "bg-[color:var(--ls-quinary-background-color)]"} (name param)) 
+     6 ({"bg" "bg-[color:var(--ls-senary-background-color)]"} (name param))
+     7 ({"bg" "bg-[color:var(--ls-border-color)]"
+         "border" "border-[color:var(--ls-border-color)]"} (name param))
+     11 ({"text" "text-[color:var(--ls-secondary-text-color)]"} (name param))
+     12 ({"text" "text-[color:var(--ls-primary-text-color)]"} (name param)))))
+
+(defn varc [color step]
+  (str "var(--color-" color "-" step ")"))
+
+(defn last-str 
+  "Given an inline AST, return the last string element you can walk to" 
+  [inline]
+  (cond 
+    (keyword? inline) (name inline)
+    (string? inline) inline
+    (coll? inline) (last-str (last inline))
+    :else (pr-str inline)))
+
+(comment
+  (last-str "A")
+  (last-str ["Plain" "A"])
+  (last-str [["Plain" "A"]])
+  (last-str [["Plain" "A"] 
+             [["Emphasis" [["Italic"] [["Plain" "B"]]]]]]))
+
+(defn render-cell-in-context 
+  "Some instances of the table provide us with raw data, others provide us with 
+  inline ASTs. This function renders the content appropriately, passing the AST along 
+  to map-inline if necessary."
+  [{:keys [map-inline-block int->local-time-2]} cell-data]
+  (cond 
+    (sequential? cell-data) (map-inline-block [:table :v2] cell-data)
+    (string? cell-data) cell-data
+    (keyword? cell-data) (name cell-data)
+    (boolean? cell-data) (pr-str cell-data) 
+    (number? cell-data) (if-let [date (int->local-time-2 cell-data)]
+                          date cell-data)))
+
+(defn map-with-all-indices [data]
+  (let [!row-index (volatile! -1)]
+    (for [[group-index group] (map-indexed vector data) 
+          [group-row-index row] (map-indexed vector group) 
+          :let [row-index (vswap! !row-index inc)]]
+      [group-index group-row-index row-index group row])))
+
+(defn get-columns [block data]
+  (->> (or (some-> (get-in block [:block/properties :logseq.table.cols])
+                   (str/split #", ?"))
+           (map last-str (ffirst data)))
+       (map (comp str/lower-case str/trim))))
+
+(defn cell-bg-classes 
+  "We track the cell the cursor last entered and update the cells according to the configured 
+  hover preference: cell, row, col, both, or none.
+  We also have to account for the header cells and stripes cells"
+  [{:keys [row-index col-index hover header? gray color stripes? cursor]}]
+  (let [;; check how the cursor position overlaps with the current cell
+        row-highlighted?  (= row-index (second cursor))
+        col-highlighted?  (= col-index (first cursor))
+        cell-highlighted? (and row-highlighted? col-highlighted?)
+        ;; check how the cell needs to be highlighted
+        highlight-row?    (and row-highlighted? (#{"row" "both"} hover))
+        highlight-col?    (and col-highlighted? (#{"col" "both"} hover))
+        highlight-cell?   (and cell-highlighted? (#{"cell" "row" "col" "both"} hover))]
+    (cond 
+      highlight-cell? (if header? (lsx 6) (lsx 4))
+      highlight-row?  (if header? (lsx 5) (lsx 3))
+      highlight-col?  (if header? (lsx 5) (lsx 3))
+      header? (lsx 4) 
+      (and stripes? (even? row-index)) (lsx 2)
+      :else (lsx 1))))
+
+(defn cell-rounded-classes 
+  "Depending on where the cell is, and whether there is a gradient accent, we need to round specific corners 
+   The cond-> is used to account for single row or single column talbes that may have multiple rounded corners."
+  [{:keys [color row-index col-index total-rows total-cols]}]
+  (let [no-gradient-accent? (nil? color)]
+    (cond-> ""
+      (and no-gradient-accent? (= [row-index col-index] [0 0])) (str " rounded-tl")
+      (and no-gradient-accent? (= [row-index col-index] [0 (dec total-cols)])) (str " rounded-tr")
+      (= [row-index col-index] [(dec total-rows) 0]) (str " rounded-bl")
+      (= [row-index col-index] [(dec total-rows) (dec total-cols)]) (str " rounded-br"))))
+
+(defn cell-text-transform-classes [{:keys [headers header?]}]
+  (when header?
+    (cond-> (get #{"uppercase" "capitalize" "lowercase" "none" "capitalize-first"} headers "none")
+      (= headers "capitalize-first") (str " lowercase"))))
+
+(defn cell-padding-classes [{:keys [compact? header?]}]
+  (cond 
+    #_compact_th (and compact? header?) (str "px-[" CELL_PADDING_COMPACT "rem] py-0.5") 
+    #_compact_td compact?               (str "px-[" CELL_PADDING_COMPACT "rem] py-0.5")
+    #_padded_th  header?                (str "px-[" CELL_PADDING "rem] py-1.5")
+    #_padded_td  :else                  (str "px-[" CELL_PADDING "rem] py-2")))
+
+(defn cell-text-classes [{:keys [header?]}]
+  (if header?
+    (str (lsx :text 11) " text-sm tracking-wide font-bold")
+    (str (lsx :text 12) " text-base")))
+
+(defn cell-classes [table-opts]
+  (str/join " "
+    [(cell-bg-classes table-opts)
+     (cell-rounded-classes table-opts)
+     (cell-text-classes table-opts)
+     (cell-text-transform-classes table-opts)
+     (cell-padding-classes table-opts)]))
+
+;; -- Handlers -----------------------------------------------------------------
+
+(defn handle-cell-pointer-down [e {:keys [cell-focus col-index row-index]}] 
+  (when (not= cell-focus [col-index row-index])
+    (.stopPropagation e) 
+    (.preventDefault e)))
+
+(defn handle-cell-click 
+  "When a cell is clicked, we need to update the cursor position and the selected cells"
+  [e {:keys [cell-focus set-cell-focus header? col-index row-index]} cell-ref]
+  ; (.stopPropagation e) 
+  (.preventDefault e)
+  (when-not (= cell-focus [col-index row-index]) 
+    (set-cell-focus [col-index row-index])))
+    
+
+(defn handle-cell-keydown 
+  "When a cell is focused, we need to update the cursor position and the selected cells"
+  [e {:keys [cell-focus set-cell-focus header? col-index row-index total-rows total-cols]}]
+  (when (= cell-focus [col-index row-index]) 
+    (and (case (.-key e)
+           "ArrowUp"    (if (= row-index 0)
+                          (set-cell-focus [col-index row-index])
+                          (set-cell-focus [col-index (dec row-index)]))
+           "ArrowDown"  (if (= row-index (dec total-rows)) 
+                          (set-cell-focus [col-index row-index])
+                          (set-cell-focus [col-index (inc row-index)]))
+           "ArrowLeft"  (cond 
+                          ;; if we are in the top left, then do not move the focus
+                          (and (= col-index 0) (= row-index 0))
+                          (set-cell-focus [col-index row-index])
+                          ;; if we are in the first column, then move to the last column of the previous row
+                          (= col-index 0) 
+                          (set-cell-focus [(dec total-cols) (dec row-index)])
+                          ;; otherwise, move to the previous column
+                          :else
+                          (set-cell-focus [(dec col-index) row-index]))
+           "ArrowRight" (cond 
+                          ;; if we are in the bottom right, then do not move the focus
+                          (and (= col-index (dec total-cols)) (= row-index (dec total-rows))) 
+                          (set-cell-focus [col-index row-index])
+                          ;; if we are in the last column, then move to the first column of the next row
+                          (= col-index (dec total-cols)) 
+                          (set-cell-focus [0 (inc row-index)])
+                          ;; otherwise, move to the next column
+                          :else
+                          (set-cell-focus [(inc col-index) row-index]))
+           nil)
+         ;; Prevent default actions when the table handles it itself
+         (.preventDefault e)
+         (.stopPropagation e))))
+        
+
+;; -- Hooks --------------------------------------------------------------------
+
+(defn use-atom 
+  "A hook that wraps use-state to allow for interaction with 
+  the state as if it were an atom"
+  [initial-value]
+  (let [atom-ref (rum/use-ref (atom initial-value))
+        atom-current (.. atom-ref -current)
+        [state set-state] (rum/use-state initial-value)]
+    (rum/use-effect! (fn [] 
+                       (set-state @atom-current) 
+                       identity)
+                     [atom-current])
+    [state atom-current]))
+
+(defn use-dynamic-widths [data]
+  (let [[static atomic] (use-atom {})
+        add-column-width (fn [col-index width]
+                           (when (< (get @atomic col-index 0) (min MAX_WIDTH width))
+                             (swap! atomic assoc col-index (min MAX_WIDTH width))
+                             ;; rum is complaining that we can only return teardown functions
+                             identity))]
+    ;; Reset the minimum widths when the data changes
+    (rum/use-effect! (fn [] (reset! atomic {}) identity)
+                     [data])
+    [static add-column-width]))
+
+(defn use-table-flow-at-width [table-px max-cols-px]
+  (let [[overflow set-overflow] (rum/use-state false)
+        [underflow set-underflow] (rum/use-state false)
+        handle-container-width (fn [container-px]
+                                 (set-underflow (< max-cols-px container-px))
+                                 (set-overflow (< container-px table-px)))]
+    [overflow underflow handle-container-width]))
+
+;; -- Components (V2) -----------------------------------------------------------
+
+(rum/defc table-scrollable-overflow [handle-root-width-change child]
+  (let [[set-root-ref root-rect root-ref] (use-ref-bounding-client-rect)
+        main-content-rect (use-dom-bounding-client-rect ($main-content))
+        
+        left-adjustment (- (:left root-rect) (:left main-content-rect))
+        right-adjustment (- (:width main-content-rect) 
+                            (- (:right root-rect) (:left main-content-rect)))
+
+        ;; Because in a scrollable container, we need to account for the scrollbar being clicked,
+        ;; we add a handler to prevent the table from switching to the input on click. 
+        ;; This also prevents the table from switching to eiditng mode when the left or right area 
+        ;; of the table is clicked, but that feels natural to me.
+        handle-pointer-down (fn [e]
+                              (when (= root-ref (.. e -target -parentElement))
+                                (.preventDefault e)))]
+    (rum/use-effect! #(handle-root-width-change (:width root-rect)) [(:width root-rect)])
+    [:div {:ref set-root-ref}
+     [:div {:style {:width (:width main-content-rect)
+                    :margin-left (- (:left main-content-rect) (:left root-rect))
+                    :padding-left left-adjustment
+                    :padding-right right-adjustment
+                    :overflow-x "scroll"}
+            :class "mt-2"
+            :on-pointer-down handle-pointer-down}
+      child]]))
+
+(rum/defc table-gradient-accent [{:keys [color]}]
+  [:div.rounded-t.h-2.-ml-px.-mt-px.-mr-px 
+   {:style {:grid-column "1 / -1" :order -999} 
+    :class (str "grad-bg-" color "-9")
+    :data-testid "v2-table-gradient-accent"}])
+
+(rum/defc table-header-row [handle-cell-width-change cells {:keys [cell-col-map] :as opts}]
+  [:<>
+   (for [[cell-index cell] (map-indexed vector cells)
+         :let [col-index (get cell-col-map cell-index)]
+         :when col-index]
+     ^{:key cell-index}
+     (table-cell handle-cell-width-change cell (assoc opts :cell-index cell-index :col-index col-index :header? true)))])
+ 
+(rum/defc table-data-row [handle-cell-width-change cells {:keys [cell-col-map] :as opts}]
+  [:<>
+   (for [[cell-index cell] (map-indexed vector cells)
+         :let [col-index (get cell-col-map cell-index)]
+         :when col-index]
+     ^{:key cell-index}
+     (table-cell handle-cell-width-change cell (assoc opts :cell-index cell-index :col-index col-index)))])
+
+(rum/defc table-cell [handle-cell-width-change cell {:keys [row-index col-index render-cell show-separator? total-cols set-cell-hover cell-focus table-underflow?] :as opts}]
+  (let [cell-ref (rum/use-ref nil)
+        cell-order (+ (* row-index total-cols) col-index)
+        static-width (get-in opts [:static-widths col-index])
+        dynamic-width (when-not static-width 
+                        (get-in opts [:dynamic-widths col-index]))]
+    ;; Whenever the cell changes, we need to calculate new bounds for the given content 
+    ;; -innerText is used here to strip out formatting, this may turn out to not work for all given block types
+    (rum/use-layout-effect! #(->> (.. cell-ref -current -innerText) 
+                                  (count) 
+                                  (handle-cell-width-change col-index))
+                            [cell])
+
+    ;; Whenever the cell becomes focused, we set it's tabIndex. When the tabIndex is set, call focus on the element 
+    (rum/use-layout-effect! #(when (= cell-focus [col-index row-index])
+                               ; (.. cell-ref -current -tabIndex 0)
+                               (some-> cell-ref .-current .focus))
+                               ; (.execCommand js/document "selectAll"))
+                            [cell-focus])
+    [:div {:ref cell-ref
+           :class (cell-classes opts)
+           :style (cond-> {:box-sizing :border-box}
+                    (not table-underflow?) (assoc :max-width (str MAX_WIDTH "rem"))
+                    static-width  (assoc :width (str static-width "rem"))
+                    dynamic-width (assoc :min-width (str (max MIN_WIDTH dynamic-width) "rem"))
+                    cell-order    (assoc :order cell-order)
+                    show-separator? (assoc :margin-top 3))
+           :tab-index (when (= cell-focus [col-index row-index]) "-1")
+           :on-pointer-enter #(set-cell-hover [col-index row-index])
+           :on-click #(handle-cell-click % opts cell-ref)
+           :on-pointer-down #(handle-cell-pointer-down % opts)
+           ; :on-pointer-up handle-cell-interrupt
+           :on-key-down #(handle-cell-keydown % opts)}
+     (render-cell cell)]))
+
+(rum/defc table-container [{:keys [columns borders? table-overflow? total-table-width gray set-cell-hover] :as opts} & children]
+  (let [grid-template-columns (str "repeat(" (count columns) ", minmax(max-content, 1fr))")]
+    [:div.grid.border.rounded {:style {:grid-template-columns grid-template-columns
+                                       :gap (when borders? BORDER_WIDTH) 
+                                       :width (when table-overflow? total-table-width)}
+                               :class (str (lsx 7) " " (lsx :border 7))
+                               :data-testid "v2-table-container"
+                               :on-pointer-leave #(set-cell-hover [])}
+     children]))
+
+(rum/defc root
+  [{:keys [data] :as _props} {:keys [block] :as context}]
+  (let [;; In order to highlight cells in the same row or column of the hovered cell, 
+        ;; we need to know the row and column that the cursor is in
+        [[_cell-hover-x _cell-hover-y :as cell-hover] set-cell-hover] (rum/use-state [])
+        [[_cell-focus-x _cell-focus-y :as cell-focus] set-cell-focus] (rum/use-state [])
+
+        ;; Depending on the content of the table, we roughly adjust the width of the column
+        ;; to do this we need to keep track of the .innerText.length of each cell and update 
+        ;; it whenever it changes
+        [dynamic-widths handle-cell-width-change] (use-dynamic-widths data)
+
+        ;; We need to call into the view config several times, so we can memoize it
+        ;; TODO: insert global config here
+        get-view-prop* (partial get-view-prop context)
+                                     
+        ;; Most of the config options will be repeated and reused throughout the table, so store 
+        ;; all of it's state in a single map for consistency
+        table-opts {; user configurable properties (sometimes with defaults)
+                    :color    (get-view-prop* :logseq.color)
+                    :headers  (get-view-prop* :logseq.table.headers "none")
+                    :borders? (get-view-prop* :logseq.table.borders true)
+                    :compact? (get-view-prop* :logseq.table.compact false)
+                    :hover    (get-view-prop* :logseq.table.hover "cell")
+                    :stripes? (get-view-prop* :logseq.table.stripes false)
+                    :gray     (color->gray (get-in context [:config :logseq.color])) 
+                    :columns  (get-columns block data)
+
+                    ; non configurable properties
+                    :cell-hover cell-hover
+                    :cell-focus cell-focus
+                    :cursor (or (not-empty cell-focus) (not-empty cell-hover))
+                    :dynamic-widths dynamic-widths
+                    :render-cell (partial render-cell-in-context context)
+                    :set-cell-hover set-cell-hover
+                    :set-cell-focus set-cell-focus
+                    :total-rows (reduce + 0 (map count data))} 
+
+        ;; The total table width has to account for the borders and the padding 
+        ;; everything is tracked in rems, except for the border, since it's so small
+        cell-padding-width (* 2 (if (:compact? table-opts) CELL_PADDING_COMPACT CELL_PADDING))
+        total-border-width (* (count (:columns table-opts)) BORDER_WIDTH)
+        total-table-width (->> (vals dynamic-widths) 
+                               (map (partial + cell-padding-width)) 
+                               (reduce + 0)  
+                               (util/rem->px)
+                               (+ total-border-width)) 
+        total-max-col-width (-> (count (:columns table-opts))
+                                (* MAX_WIDTH)
+                                (util/rem->px)
+                                (+ total-border-width))
+
+        ;; The table is actually rendered differently when it needs to be scrollable. 
+        ;; Keep track of whether the ideal table size overflows it's container size, 
+        ;; and provide a handler to be called whenever the container width changes 
+        [table-overflow? table-underflow? handle-root-width-change] (use-table-flow-at-width total-table-width total-max-col-width)
+
+        ;; Because the data may come in a different order than it should be presented, 
+        ;; we need to distinguish between these and provide a conversion. 
+        ;; The order the data is stored in is referred to as the cell order.
+        ;; The order the data is displayed as is referred to as the col order.
+        ;; Since these are called on every render of every cell, and are not dynamic, they are computed up front
+        cell-col-map (->> (ffirst data) 
+                          (map-indexed (juxt #(identity %1) 
+                                             #(.indexOf (:columns table-opts) (.toLowerCase (last-str %2)))))
+                          (remove (comp #{-1} second))
+                          (into {}))
+
+        ;; There are a couple more computed table properties that are best calculated 
+        ;; after the initial object is creaated
+        table-opts (assoc table-opts :total-cols (count (:columns table-opts))
+                                     :total-table-width total-table-width
+                                     :table-overflow? table-overflow?
+                                     :table-underflow? table-underflow?
+                                     :cell-col-map cell-col-map)]
+    ; (js/console.log "shui table opts context" (clj->js context)) 
+    ; (js/console.log "shui table opts" (clj->js table-opts)) 
+    ; (js/console.log "shui table opts" (pr-str table-opts)) 
+    ;; Scrollable Container: if the table is larger than the container, manage the scrolling effects here
+    (table-scrollable-overflow handle-root-width-change
+     ;; Grid Container: control the outermost table related element (border radius, grid, etc)
+     (table-container table-opts
+      ;; Gradient Accent: the accent color at the top of the application
+      (when (:color table-opts)
+        (table-gradient-accent table-opts))
+      ;; Rows: the actual table rows
+      (for [[group-index group-row-index row-index _group row] (map-with-all-indices data)
+            :let [show-separator? (and (= 0 group-row-index) (< 1 group-index))
+                  opts (assoc table-opts :group-index group-index 
+                                         :group-row-index group-row-index 
+                                         :row-index row-index 
+                                         :show-separator? show-separator?)]]
+          (if (= 0 group-index)
+            ;; Table Header: Rows in the first section are rendered as headers
+            ^{:key row-index} (table-header-row handle-cell-width-change row opts)
+            ;; Table Body: The rest of the data is rendered as cells
+            ^{:key row-index} (table-data-row handle-cell-width-change row opts)))))))
+

+ 81 - 0
deps/shui/src/logseq/shui/util.cljs

@@ -0,0 +1,81 @@
+(ns logseq.shui.util
+  (:require 
+    [clojure.string :as s]
+    [rum.core :refer [use-state use-effect!] :as rum]
+    [goog.dom :as gdom]))
+
+
+;;      /--------------- app ------------\
+;;    /-------- left --------\             \
+;;  /l-side\                  \  /- r-side --\
+;;
+;; |--------|-------------------|-------------| \ head
+;; |--------|-------------------|             | /
+;; |        |                   |             |
+;; |        |                   |             |
+;; |        |                   |             |
+;; |--------|-------------------|-------------|
+
+(def $app           (partial gdom/getElement "app-container"))
+(def $left          (partial gdom/getElement "left-container"))
+(def $head          (partial gdom/getElement "head-container"))
+(def $main          (partial gdom/getElement "main-container"))
+(def $main-content  (partial gdom/getElement "main-content-container"))
+(def $left-sidebar  (partial gdom/getElement "left-sidebar"))
+(def $right-sidebar (partial gdom/getElement "right-sidebar"))
+
+(defn el->clj-rect [el]
+  (let [rect (.getBoundingClientRect el)]
+    {:top (.-top rect)
+     :left (.-left rect)
+     :bottom (.-bottom rect)
+     :right (.-right rect)
+     :width (.-width rect)
+     :height (.-height rect)
+     :x (.-x rect)
+     :y (.-y rect)}))
+
+(defn clj-rect-observer [update!]
+  (js/ResizeObserver.
+    (fn [entries] 
+      (when (.-contentRect (first (js->clj entries)))
+        (update!)))))
+
+(defn use-dom-bounding-client-rect
+  ([el] (use-dom-bounding-client-rect el nil))
+  ([el tick] 
+   (let [[rect set-rect] (rum/use-state nil)]
+     (rum/use-effect! 
+       (if el 
+         (fn [] 
+           (let [update! #(set-rect (el->clj-rect el))
+                 observer (clj-rect-observer update!)]
+             (update!)
+             (.observe observer el) 
+             #(.disconnect observer)))
+         #())
+       [el tick])
+     rect)))
+          
+(defn use-ref-bounding-client-rect 
+  ([] (use-ref-bounding-client-rect nil))
+  ([tick]
+   (let [[ref set-ref] (rum/use-state nil)
+         rect (use-dom-bounding-client-rect ref tick)]
+     [set-ref rect ref]))
+  ([ref tick] [nil (use-dom-bounding-client-rect ref tick)]))
+
+
+(defn rem->px [rem]
+  (-> js/document.documentElement
+      js/getComputedStyle
+      (.-fontSize)
+      (js/parseFloat)
+      (* rem)))
+
+(defn px->rem [px]
+  (->> js/document.documentElement
+       js/getComputedStyle
+       (.-fontSize)
+       (js/parseFloat)
+       (/ px)))

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

@@ -141,8 +141,8 @@ test('template', async ({ page, block }) => {
 
   await block.waitForBlocks(5)
 
-  // NOTE: use delay to type slower, to trigger auto-completion UI.
-  await block.clickNext()
+  // See-also: #9354
+  await block.enterNext()
   await block.mustType('/template')
 
   await page.click('[title="Insert a created template here"]')
@@ -154,6 +154,19 @@ test('template', async ({ page, block }) => {
   await popupMenuItem.click()
 
   await block.waitForBlocks(9)
+
+
+  await block.clickNext()
+  await block.mustType('/template')
+
+  await page.click('[title="Insert a created template here"]')
+  // type to search template name
+  await page.keyboard.type(randomTemplate.substring(0, 3), { delay: 100 })
+
+  await popupMenuItem.waitFor({ timeout: 2000 }) // wait for template search
+  await popupMenuItem.click()
+
+  await block.waitForBlocks(13) // 9 + 4
 })
 
 test('auto completion square brackets', async ({ page, block }) => {

+ 1 - 1
e2e-tests/fixtures.ts

@@ -27,7 +27,7 @@ const consoleLogWatcher = (msg: ConsoleMessage) => {
   const text = msg.text()
   logs += text + '\n'
 
-  expect(text, logs).not.toMatch(/^(Failed to|Uncaught)/)
+  expect(text, logs).not.toMatch(/^(Failed to|Uncaught|Assert failed)/)
 
   // youtube video
   // Error with Permissions-Policy header: Origin trial controlled feature not enabled: 'ch-ua-reduced'.

+ 304 - 0
e2e-tests/shui/table.spec.js

@@ -0,0 +1,304 @@
+import { expect } from '@playwright/test'
+import fs from 'fs/promises'
+import path from 'path'
+import { test } from '../fixtures'
+import { randomString, editFirstBlock, navigateToStartOfBlock, createRandomPage } from '../utils'
+
+test.setTimeout(60000)
+
+const KEY_DELAY = 100
+
+// The following function assumes that the block is currently in edit mode, 
+// and it just enters a simple table
+const inputSimpleTable = async (page) => {
+  await page.keyboard.type('| Header A | Header B |')
+  await page.keyboard.press('Shift+Enter')
+  await page.keyboard.type('| A1 | B1 |') 
+  await page.keyboard.press('Shift+Enter')
+  await page.keyboard.type('| A2 | B2 |')
+  await page.keyboard.press('Escape')
+  await page.waitForTimeout(KEY_DELAY)
+}
+
+// The following function does not assume any state, and will prepend the provided lines to the 
+// first block of the document 
+const prependPropsToFirstBlock = async (page, block, ...props) => {
+  await editFirstBlock(page) 
+  await page.waitForTimeout(KEY_DELAY) 
+  await navigateToStartOfBlock(page, block)
+  await page.waitForTimeout(KEY_DELAY) 
+
+  for (const prop of props) {
+    await page.keyboard.type(prop)
+    await page.waitForTimeout(KEY_DELAY)
+    await page.keyboard.press('Shift+Enter')
+    await page.waitForTimeout(KEY_DELAY)
+  }
+
+  await page.keyboard.press('Escape')
+  await page.waitForTimeout(KEY_DELAY)
+}
+
+const setPropInFirstBlock = async (page, block, prop, value) => {
+  await editFirstBlock(page)
+  await page.waitForTimeout(KEY_DELAY)
+  await navigateToStartOfBlock(page, block)
+  await page.waitForTimeout(KEY_DELAY)
+
+  const inputValue = await page.inputValue('textarea >> nth=0')
+
+  const match = inputValue.match(new RegExp(`${prop}::(.*)(\n|$)`))
+
+  if (!match) {
+    await page.keyboard.press('Shift+Enter')
+    await page.waitForTimeout(KEY_DELAY)
+    await page.keyboard.press('ArrowUp')
+    await page.waitForTimeout(KEY_DELAY)
+    await page.keyboard.type(`${prop}:: ${value}`)
+    // await page.waitForTimeout(1000)
+    // await page.waitForTimeout(KEY_DELAY)
+    // await page.keyboard.type(prop + ':: ' + value)
+    // await page.waitForTimeout(1000)
+    // await page.keyboard.press('Shift+Enter')
+    await page.waitForTimeout(KEY_DELAY)
+    await page.keyboard.press('Escape')
+    return await page.waitForTimeout(KEY_DELAY)
+  }
+
+  const [propLine, propValue, propTernary] = match
+  const startIndex = match.index
+  const endIndex = startIndex + propLine.length - propTernary.length
+
+  // Go to the of the prop
+  for (let i = 0; i < endIndex; i++) {
+    await page.keyboard.press('ArrowRight')
+  }
+
+  // Delete the value of the prop 
+  for (let i = 0; i < propValue.length; i++) {
+    await page.keyboard.press('Backspace')
+  }
+
+  // Input the new value of the prop
+  await page.keyboard.type(" " + value.trim())
+  await page.waitForTimeout(KEY_DELAY)
+  await page.keyboard.press('Escape')
+  return await page.waitForTimeout(KEY_DELAY)
+}
+
+
+test('table can have it\'s version changed via props', async ({ page, block, graphDir }) => {
+  const pageTitle = await createRandomPage(page)
+
+  // create a v1 table 
+  inputSimpleTable(page)
+
+  // find and confirm existence of first data cell
+  await expect(await page.locator('table tbody tr >> nth=0').innerHTML()).toContain('A1</td>')
+
+  // change to a version 2 table
+  await setPropInFirstBlock(page, block, 'logseq.table.version', '2')
+
+  // find and confirm existence of first data cell in new format
+  await expect(await page.getByTestId('v2-table-container').innerHTML()).toContain('A1</div>')
+})
+
+test('table can configure logseq.color::', async ({ page, block, graphDir }) => {
+  const pageTitle = await createRandomPage(page)
+
+  // create a v1 table 
+  await page.keyboard.type('logseq.table.version:: 2')
+  await page.keyboard.press('Shift+Enter')
+  await inputSimpleTable(page)
+
+  // check for default general config 
+  await expect(await page.getByTestId('v2-table-gradient-accent')).not.toBeVisible()
+
+  await setPropInFirstBlock(page, block, 'logseq.color', 'red')
+
+  // check for gradient accent 
+  await expect(await page.getByTestId('v2-table-gradient-accent')).toBeVisible()
+})
+
+test('table can configure logseq.table.hover::', async ({ page, block, graphDir }) => {
+  const pageTitle = await createRandomPage(page)
+
+  // create a v1 table 
+  await page.keyboard.type('logseq.table.version:: 2')
+  await page.keyboard.press('Shift+Enter')
+  await inputSimpleTable(page)
+
+  await page.waitForTimeout(KEY_DELAY)
+  await page.getByText('A1', { exact: true }).hover()
+  await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]')
+  await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+
+  await setPropInFirstBlock(page, block, 'logseq.table.hover', 'row')
+
+  await page.waitForTimeout(KEY_DELAY)
+  await page.getByText('A1', { exact: true }).hover()
+  await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]')
+  await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+
+  await setPropInFirstBlock(page, block, 'logseq.table.hover', 'col')
+
+  await page.waitForTimeout(KEY_DELAY)
+  await page.getByText('A1', { exact: true }).hover()
+  await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]')
+  await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+
+  await setPropInFirstBlock(page, block, 'logseq.table.hover', 'both')
+
+  await page.waitForTimeout(KEY_DELAY)
+  await page.getByText('A1', { exact: true }).hover()
+  await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-quaternary-background-color)]')
+  await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+
+  await setPropInFirstBlock(page, block, 'logseq.table.hover', 'none')
+
+  await page.waitForTimeout(KEY_DELAY)
+  await page.getByText('A1', { exact: true }).hover()
+  await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-quaternary-background-color)]')
+  await expect(await page.getByText('B1', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+  await expect(await page.getByText('B2', { exact: true }).getAttribute('class')).not.toContain('bg-[color:var(--ls-tertiary-background-color)]')
+})
+
+test('table can configure logseq.table.headers', async ({ page, block, graphDir }) => {
+  const pageTitle = await createRandomPage(page)
+
+  // create a table
+  await page.keyboard.type('logseq.table.version:: 2')
+  await page.keyboard.press('Shift+Enter')
+  await inputSimpleTable(page)
+
+  // Check none (default)
+  await expect(await page.getByText('Header A', { exact: true })).toBeVisible()
+  await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header A")
+
+  // Check none (explicit)
+  await setPropInFirstBlock(page, block, 'logseq.table.headers', 'none')
+  await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header A")
+
+  // Check uppercase
+  await setPropInFirstBlock(page, block, 'logseq.table.headers', 'uppercase')
+  await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("HEADER A")
+
+  // Check lowercase
+  await setPropInFirstBlock(page, block, 'logseq.table.headers', 'lowercase')
+  await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("header a")
+
+  // Check capitalize
+  await setPropInFirstBlock(page, block, 'logseq.table.headers', 'capitalize')
+  await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header A")
+
+  // Check capitalize-first
+  await setPropInFirstBlock(page, block, 'logseq.table.headers', 'capitalize-first')
+  await expect(await page.getByText('Header A', { exact: true }).innerText()).toEqual("Header a")
+})
+
+test('table can configure logseq.table.borders', async ({ page, block, graphDir }) => {
+  const pageTitle = await createRandomPage(page)
+
+  // create a table
+  await page.keyboard.type('logseq.table.version:: 2')
+  await page.keyboard.press('Shift+Enter')
+  await inputSimpleTable(page)
+
+  // Check true (default)
+  await expect(await page.getByTestId('v2-table-container')).toHaveCSS("gap", /^[1-9].*/)
+
+  // Check true (explicit)
+  await setPropInFirstBlock(page, block, 'logseq.table.borders', 'true')
+  await expect(await page.getByTestId('v2-table-container')).toHaveCSS("gap", /^[1-9].*/)
+
+  // Check false
+  await setPropInFirstBlock(page, block, 'logseq.table.borders', 'false')
+  await expect(await page.getByTestId('v2-table-container')).not.toHaveCSS("gap", /^[1-9].*/)
+})
+
+test('table can configure logseq.table.stripes', async ({ page, block, graphDir }) => {
+  const pageTitle = await createRandomPage(page)
+
+  // create a table
+  await page.keyboard.type('logseq.table.version:: 2')
+  await page.keyboard.press('Shift+Enter')
+  await inputSimpleTable(page)
+  await page.waitForTimeout(KEY_DELAY)
+
+  // Check false (default)
+  await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
+  await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
+
+  // Check false (explicit)
+  await setPropInFirstBlock(page, block, 'logseq.table.stripes', 'false')
+  await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
+  await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
+
+  // Check false
+  await setPropInFirstBlock(page, block, 'logseq.table.stripes', 'true')
+  await expect(await page.getByText('A1', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-primary-background-color)]")
+  await expect(await page.getByText('A2', { exact: true }).getAttribute('class')).toContain("bg-[color:var(--ls-secondary-background-color)]")
+})
+
+test('table can configure logseq.table.compact', async ({ page, block, graphDir }) => {
+  const pageTitle = await createRandomPage(page)
+
+  // create a table
+  await page.keyboard.type('logseq.table.version:: 2')
+  await page.keyboard.press('Shift+Enter')
+  await inputSimpleTable(page)
+  await page.waitForTimeout(KEY_DELAY)
+
+  // Check false (default)
+  const defaultClasses = await page.getByText('A1', { exact: true }).getAttribute('class')
+
+  // Check false (explicit)
+  await setPropInFirstBlock(page, block, 'logseq.table.compact', 'false')
+  const falseClasses = await page.getByText('A1', { exact: true }).getAttribute('class')
+
+  // Check false
+  await setPropInFirstBlock(page, block, 'logseq.table.compact', 'true')
+  const trueClasses = await page.getByText('A1', { exact: true }).getAttribute('class')
+
+  const getPX = (str) => {
+    const match = str.match(/px-\[([0-9\.]*)[a-z]*\]/)
+    return match ? parseFloat(match[1]) : null
+  }
+
+  await expect(getPX(defaultClasses)).toEqual(getPX(falseClasses))
+  await expect(getPX(defaultClasses)).toBeGreaterThan(getPX(trueClasses))
+})
+
+test('table can configure logseq.table.cols::', async ({ page, block, graphDir }) => {
+  const pageTitle = await createRandomPage(page)
+
+  // create a v1 table 
+  await page.keyboard.type('logseq.table.version:: 2')
+  await page.keyboard.press('Shift+Enter')
+  await inputSimpleTable(page)
+
+  // check for default general config 
+  await expect(await page.getByText('A1', { exact: true })).toBeVisible()
+  await expect(await page.getByText('B1', { exact: true })).toBeVisible()
+
+  await setPropInFirstBlock(page, block, 'logseq.table.cols', 'Header A, Header B')
+  await expect(await page.getByText('A1', { exact: true })).toBeVisible()
+  await expect(await page.getByText('B1', { exact: true })).toBeVisible()
+
+  await setPropInFirstBlock(page, block, 'logseq.table.cols', 'Header A')
+  await expect(await page.getByText('A1', { exact: true })).toBeVisible()
+  await expect(await page.getByText('B1', { exact: true })).not.toBeVisible()
+
+  await setPropInFirstBlock(page, block, 'logseq.table.cols', 'Header B')
+  await expect(await page.getByText('A1', { exact: true })).not.toBeVisible()
+  await expect(await page.getByText('B1', { exact: true })).toBeVisible()
+})

+ 7 - 0
e2e-tests/utils.ts

@@ -206,3 +206,10 @@ export async function getIsWebAPIClipboardSupported(page: Page): Promise<boolean
   // @ts-ignore "clipboard-write" is not included in TS's type definition for permissionName
   return await queryPermission(page, "clipboard-write") && await doesClipboardItemExists(page)
 }
+
+export async function navigateToStartOfBlock(page: Page, block: Block) {
+  const selectionStart = await block.selectionStart()
+  for (let i = 0; i < selectionStart; i++) {
+    await page.keyboard.press('ArrowLeft')
+  }
+}

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

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

+ 2 - 0
package.json

@@ -93,6 +93,7 @@
         "@logseq/capacitor-file-sync": "0.0.24",
         "@logseq/diff-merge": "^0.0.2",
         "@logseq/react-tweet-embed": "1.3.1-1",
+        "@radix-ui/colors": "^0.1.8",
         "@sentry/react": "^6.18.2",
         "@sentry/tracing": "^6.18.2",
         "@tabler/icons": "^1.96.0",
@@ -133,6 +134,7 @@
         "remove-accents": "0.4.2",
         "sanitize-filename": "1.6.3",
         "send-intent": "3.0.11",
+        "tailwind-capitalize-first-letter": "^1.0.4",
         "threads": "1.6.5",
         "url": "^0.11.0",
         "yargs-parser": "20.2.4"

+ 1 - 0
public/index.html

@@ -55,6 +55,7 @@
 <script defer src="/static/js/main.js"></script>
 <script defer src="/static/js/amplify.js"></script>
 <script defer src="/static/js/tabler.min.js"></script>
+<script defer src="/static/js/tabler.ext.js"></script>
 <script defer src="/static/js/code-editor.js"></script>
 <script defer src="/static/js/tldraw.js"></script>
 <script defer src="/static/js/excalidraw.js"></script>

+ 1 - 0
resources/electron.html

@@ -56,6 +56,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/main.js"></script>
 <script defer src="./js/amplify.js"></script>
 <script defer src="./js/tabler.min.js"></script>
+<script defer src="./js/tabler.ext.js"></script>
 <script defer src="./js/code-editor.js"></script>
 <script defer src="./js/excalidraw.js"></script>
 <script defer src="./js/tldraw.js"></script>

+ 1 - 0
resources/index.html

@@ -55,6 +55,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/main.js"></script>
 <script defer src="./js/amplify.js"></script>
 <script defer src="./js/tabler.min.js"></script>
+<script defer src="./js/tabler.ext.js"></script>
 <script defer src="./js/code-editor.js"></script>
 <script defer src="./js/excalidraw.js"></script>
 <script defer src="./js/tldraw.js"></script>

文件差异内容过多而无法显示
+ 10 - 0
resources/js/tabler.ext.js


+ 1 - 1
resources/package.json

@@ -1,7 +1,7 @@
 {
   "name": "Logseq",
   "productName": "Logseq",
-  "version": "0.9.5",
+  "version": "0.9.6",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",

+ 6 - 6
shadow-cljs.edn

@@ -21,8 +21,8 @@
                          :depends-on #{:main}}
                         :tldraw
                         {:entries    [frontend.extensions.tldraw]
-                         :depends-on #{:main}}
-                        }
+                         :depends-on #{:main}}}
+                        
         :output-dir       "./static/js"
         :asset-path       "/static/js"
         :release          {:asset-path "https://asset.logseq.com/static/js"}
@@ -95,8 +95,8 @@
                                 :depends-on #{:main}}
                                :tldraw
                                {:entries    [frontend.extensions.tldraw]
-                                :depends-on #{:main}}
-                               }
+                                :depends-on #{:main}}}
+                               
                :output-dir       "./static/js/publishing"
                :asset-path       "static/js"
                :closure-defines  {frontend.config/PUBLISHING true
@@ -109,5 +109,5 @@
                                                        :redef false}}
                :devtools         {:before-load frontend.core/stop
                                   :after-load  frontend.core/start
-                                  :preloads    [devtools.preload]}}
-  }}
+                                  :preloads    [devtools.preload]}}}}
+  

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

@@ -236,10 +236,10 @@
      (cond
        (and (util/electron?) (config/local-db? (state/get-current-repo)))
 
-       ["Upload an asset" [[:editor/click-hidden-file-input :id]] "Upload file types like image, pdf, docx, etc.)"]
+       ["Upload an asset" [[:editor/click-hidden-file-input :id]] "Upload file types like image, pdf, docx, etc.)"])]
 
        ;; ["Upload an image" [[:editor/click-hidden-file-input :id]]]
-       )]
+       
 
     (headings)
 

+ 72 - 64
src/main/frontend/components/block.cljs

@@ -23,8 +23,8 @@
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.db :as db]
-            [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as model]
+            [frontend.db-mixins :as db-mixins]
             [frontend.extensions.highlight :as highlight]
             [frontend.extensions.latex :as latex]
             [frontend.extensions.lightbox :as lightbox]
@@ -52,6 +52,7 @@
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.tree :as tree]
             [frontend.security :as security]
+            [frontend.shui :refer [get-shui-component-version make-shui-context]]
             [frontend.state :as state]
             [frontend.template :as template]
             [frontend.ui :as ui]
@@ -73,6 +74,7 @@
             [logseq.graph-parser.util.block-ref :as block-ref]
             [logseq.graph-parser.util.page-ref :as page-ref]
             [logseq.graph-parser.whiteboard :as gp-whiteboard]
+            [logseq.shui.core :as shui]
             [medley.core :as medley]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
@@ -292,8 +294,8 @@
                          (js/setTimeout #(reset! *resizing-image? false) 200)))
           :onClick (fn [e]
                      (when @*resizing-image? (util/stop e)))}
-          (and (:width metadata) (not (util/mobile?)))
-          (assoc :style {:width (:width metadata)}))
+         (and (:width metadata) (not (util/mobile?)))
+         (assoc :style {:width (:width metadata)}))
         {})
       [:div.asset-container {:key "resize-asset-container"}
        [:img.rounded-sm.relative
@@ -926,8 +928,8 @@
              inner)])
         [:span.warning.mr-1 {:title "Block ref invalid"}
          (block-ref/->block-ref id)]))
-  [:span.warning.mr-1 {:title "Block ref invalid"}
-    (block-ref/->block-ref id)]))
+    [:span.warning.mr-1 {:title "Block ref invalid"}
+      (block-ref/->block-ref id)]))
 
 (defn inline-text
   ([format v]
@@ -1106,8 +1108,8 @@
         {:href      (path/path-join "file://" path)
          :data-href path
          :target    "_blank"}
-         title
-         (assoc :title title))
+        title
+        (assoc :title title))
        (map-inline config label)))
 
     :else
@@ -1200,8 +1202,8 @@
            (cond->
             {:href (ar-url->http-url href)
              :target "_blank"}
-             title
-             (assoc :title title))
+            title
+            (assoc :title title))
            (map-inline config label))
 
           :else
@@ -1210,11 +1212,11 @@
            (cond->
             {:href href
              :target "_blank"}
-             title
-             (assoc :title title))
+            title
+            (assoc :title title))
            (map-inline config label)))))))
 
-(declare ->hiccup)
+(declare ->hiccup inline)
 
 (defn wrap-query-components
   [config]
@@ -1223,7 +1225,8 @@
           :->elem ->elem
           :page-cp page-cp
           :inline-text inline-text
-          :map-inline map-inline}))
+          :map-inline map-inline
+          :inline inline}))
 
 ;;;; Macro component render functions
 (defn- macro-query-cp
@@ -1738,7 +1741,7 @@
         order-list?        (boolean own-number-list?)
         order-list-idx     (:own-order-list-index config)
         collapsable?       (editor-handler/collapsable? uuid {:semantic? true})]
-    [:div.block-control-wrap.mr-1.flex.flex-row.items-center.sm:mr-2
+    [:div.block-control-wrap.flex.flex-row.items-center
      {:class (util/classnames [{:is-order-list order-list?
                                 :bullet-closed collapsed?}])}
      (when (or (not fold-button-right?) collapsable?)
@@ -2247,7 +2250,7 @@
              (and (not block-content?)
                   (seq (:block/children block))
                   (= move-to :nested)))
-          (dnd-separator move-to block-content?))))))
+         (dnd-separator move-to block-content?))))))
 
 (defn clock-summary-cp
   [block body]
@@ -2312,19 +2315,19 @@
         content (if (string? content) (string/trim content) "")
         mouse-down-key (if (util/ios?)
                          :on-click
-                         :on-mouse-down ; TODO: it seems that Safari doesn't work well with on-mouse-down
-                         )
+                         :on-mouse-down) ; TODO: it seems that Safari doesn't work well with on-mouse-down
+                         
         attrs (cond->
                {:blockid       (str uuid)
                 :data-type (name block-type)
                 :style {:width "100%" :pointer-events (when stop-events? "none")}}
 
-                (not (string/blank? (:hl-color properties)))
-                (assoc :data-hl-color (:hl-color properties))
+               (not (string/blank? (:hl-color properties)))
+               (assoc :data-hl-color (:hl-color properties))
 
-                (not block-ref?)
-                (assoc mouse-down-key (fn [e]
-                                        (block-content-on-mouse-down e block block-id content edit-input-id))))]
+               (not block-ref?)
+               (assoc mouse-down-key (fn [e]
+                                       (block-content-on-mouse-down e block block-id content edit-input-id))))]
     [:div.block-content.inline
      (cond-> {:id (str "block-content-" uuid)
               :on-mouse-up (fn [e]
@@ -3002,8 +3005,8 @@
          :li
          (cond->
           {:checked checked?}
-           number
-           (assoc :value number))
+          number
+          (assoc :value number))
          (vec-cat
           [(->elem
             :p
@@ -3021,52 +3024,55 @@
 
 (defn table
   [config {:keys [header groups col_groups]}]
-  (let [tr (fn [elm cols]
-             (->elem
-              :tr
-              (mapv (fn [col]
-                      (->elem
-                       elm
-                       {:scope "col"
-                        :class "org-left"}
-                       (map-inline config col)))
-                    cols)))
-        tb-col-groups (try
-                        (mapv (fn [number]
-                                (let [col-elem [:col {:class "org-left"}]]
-                                  (->elem
-                                   :colgroup
-                                   (repeat number col-elem))))
-                              col_groups)
-                        (catch :default _e
-                          []))
-        head (when header
-               [:thead (tr :th header)])
-        groups (mapv (fn [group]
-                       (->elem
-                        :tbody
-                        (mapv #(tr :td %) group)))
-                     groups)]
-    [:div.table-wrapper
-     (->elem
-      :table
-      {:class "table-auto"
-       :border 2
-       :cell-spacing 0
-       :cell-padding 6
-       :rules "groups"
-       :frame "hsides"}
-      (vec-cat
-       tb-col-groups
-       (cons head groups)))]))
+  (case (get-shui-component-version :table config) 
+    2 (shui/table-v2 {:data (concat [[header]] groups)} 
+                     (make-shui-context config inline))
+    1 (let [tr (fn [elm cols]
+                 (->elem
+                  :tr
+                  (mapv (fn [col]
+                          (->elem
+                           elm
+                           {:scope "col"
+                            :class "org-left"}
+                           (map-inline config col)))
+                        cols)))
+            tb-col-groups (try
+                            (mapv (fn [number]
+                                    (let [col-elem [:col {:class "org-left"}]]
+                                      (->elem
+                                       :colgroup
+                                       (repeat number col-elem))))
+                                  col_groups)
+                            (catch :default _e
+                              []))
+            head (when header
+                   [:thead (tr :th header)])
+            groups (mapv (fn [group]
+                           (->elem
+                            :tbody
+                            (mapv #(tr :td %) group)))
+                         groups)]
+        [:div.table-wrapper
+         (->elem
+          :table
+          {:class "table-auto"
+           :border 2
+           :cell-spacing 0
+           :cell-padding 6
+           :rules "groups"
+           :frame "hsides"}
+          (vec-cat
+           tb-col-groups
+           (cons head groups)))])))
 
 (defn logbook-cp
   [log]
   (let [clocks (filter #(string/starts-with? % "CLOCK:") log)
-        clocks (reverse (sort-by str clocks))
+        clocks (reverse (sort-by str clocks))]
         ;; TODO: display states change log
         ; states (filter #(not (string/starts-with? % "CLOCK:")) log)
-        ]
+        
     (when (seq clocks)
       (let [tr (fn [elm cols] (->elem :tr
                                       (mapv (fn [col] (->elem elm col)) cols)))
@@ -3091,6 +3097,8 @@
   [config col]
   (map #(inline config %) col))
 
+(declare ->hiccup)
+
 (rum/defc src-cp < rum/static
   [config options html-export?]
   (when options

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

@@ -202,6 +202,7 @@
 
 .block-control-wrap {
   height: 24px;
+  min-width: 50px;
   margin-top: 0;
 
   &.is-order-list {
@@ -531,7 +532,7 @@
   }
 
   &.as-order-list {
-    @apply w-[28px] whitespace-nowrap justify-start;
+    @apply w-[28px] whitespace-nowrap justify-start pl-[3px];
   }
 
   .bullet {

+ 12 - 11
src/main/frontend/components/page.cljs

@@ -262,7 +262,7 @@
                     :else
                     (state/set-modal! (confirm-fn)))
                   (util/stop e))]
-    [:span.absolute.inset-0.edit-input-wrapper
+    [:span.absolute.inset-0.edit-input-wrapper.z-10
      {:class (util/classnames [{:editing @*edit?}])}
      [:input.edit-input
       {:type          "text"
@@ -317,16 +317,17 @@
                          (when (util/right-click? e)
                            (state/set-state! :page-title/context {:page page-name})))
         :on-click (fn [e]
-                       (.preventDefault e)
-                       (if (gobj/get e "shiftKey")
-                         (when-let [page (db/pull repo '[*] [:block/name page-name])]
-                           (state/sidebar-add-block!
-                            repo
-                            (:db/id page)
-                            :page))
-                         (when (and (not hls-page?) (not fmt-journal?) (not config/publishing?))
-                           (reset! *input-value (if untitled? "" old-name))
-                           (reset! *edit? true))))}
+                    (when-not (= (.-nodeName (.-target e)) "INPUT")
+                      (.preventDefault e)
+                      (if (gobj/get e "shiftKey")
+                        (when-let [page (db/pull repo '[*] [:block/name page-name])]
+                          (state/sidebar-add-block!
+                           repo
+                           (:db/id page)
+                           :page))
+                        (when (and (not hls-page?) (not fmt-journal?) (not config/publishing?))
+                          (reset! *input-value (if untitled? "" old-name))
+                          (reset! *edit? true)))))}
        (when (not= icon "") [:span.page-icon icon])
        [:div.page-title-sizer-wrapper.relative
         (when @*edit?

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

@@ -249,6 +249,7 @@
     box-shadow: none;
     padding-left: 5px;
     padding-top: 5px;
+    padding-bottom: 4px;
 
     &-wrapper {
       @apply rounded;

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

@@ -836,6 +836,16 @@
     > .injected-ui-item-pagebar {
       @apply pr-3 opacity-30 hover:opacity-100 transition-opacity;
     }
+
+    > .list-wrap {
+      @apply flex items-center flex-nowrap overflow-x-hidden pt-[14px];
+    }
+
+    a.button {
+      @apply flex items-center;
+
+      color: var(--ls-primary-text-color);
+    }
   }
 
   .toolbar-plugins-manager {

+ 4 - 3
src/main/frontend/components/query.cljs

@@ -50,7 +50,7 @@
            view-f
            result
            group-by-page?]}]
-  (let [{:keys [->hiccup ->elem inline-text page-cp map-inline]} config
+  (let [{:keys [->hiccup ->elem inline-text page-cp map-inline inline]} config
         *query-error query-error-atom
         only-blocks? (:block/uuid (first result))
         blocks-grouped-by-page? (and group-by-page?
@@ -59,6 +59,7 @@
                                      (:block/name (ffirst result))
                                      (:block/uuid (first (second (first result))))
                                      true)]
+    (println "this should be a function" inline)
     (if @*query-error
       (do
         (log/error :exception @*query-error)
@@ -77,10 +78,10 @@
            (util/hiccup-keywordize result))
 
          page-list?
-         (query-table/result-table config current-block result {:page? true} map-inline page-cp ->elem inline-text)
+         (query-table/result-table config current-block result {:page? true} map-inline page-cp ->elem inline-text inline)
 
          table?
-         (query-table/result-table config current-block result {:page? false} map-inline page-cp ->elem inline-text)
+         (query-table/result-table config current-block result {:page? false} map-inline page-cp ->elem inline-text inline)
 
          (and (seq result) (or only-blocks? blocks-grouped-by-page?))
          (->hiccup result

+ 79 - 54
src/main/frontend/components/query_table.cljs

@@ -3,13 +3,15 @@
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db.query-dsl :as query-dsl]
+            [frontend.format.block :as block]
             [frontend.handler.common :as common-handler]
             [frontend.handler.editor.property :as editor-property]
+            [frontend.shui :refer [get-shui-component-version make-shui-context]]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util.clock :as clock]
             [frontend.util.property :as property]
-            [frontend.format.block :as block]
+            [logseq.shui.core :as shui]
             [medley.core :as medley]
             [rum.core :as rum]
             [logseq.graph-parser.text :as text]))
@@ -42,9 +44,9 @@
 (defn- locale-compare
   "Use locale specific comparison for strings and general comparison for others."
   [x y]
-    (if (and (number? x) (number? y))
-      (< x y)
-      (.localeCompare (str x) (str y) (state/sub :preferred-language) #js {:numeric true})))
+  (if (and (number? x) (number? y))
+    (< x y)
+    (.localeCompare (str x) (str y) (state/sub :preferred-language) #js {:numeric true})))
 
 (defn- sort-result [result {:keys [sort-by-column sort-desc? sort-nlp-date? page?]}]
   (if (some? sort-by-column)
@@ -98,7 +100,7 @@
         keys (if page? (distinct (concat keys [:created-at :updated-at])) keys)]
     keys))
 
-(defn- get-columns [current-block result {:keys [page?]}]
+(defn get-columns [current-block result {:keys [page?]}]
   (let [query-properties (some-> (get-in current-block [:block/properties :query-properties] "")
                                  (common-handler/safe-read-string "Parsing query properties failed"))
         query-properties (if page? (remove #{:block} query-properties) query-properties)
@@ -152,10 +154,21 @@
                    ;; Fallback to original properties for page blocks
                    (get-in row [:block/properties column])))]))
 
+(defn build-column-text [row column]
+  (case column 
+    :page  (or (get-in row [:block/page :block/original-name])
+               (get-in row [:block/original-name])
+               (get-in row [:block/content]))
+    :block (or (get-in row [:block/original-name]) 
+               (get-in row [:block/content])) 
+           (or (get-in row [:block/properties column])
+               (get-in row [:block/properties-text-values column])
+               (get-in row [(keyword :block column)]))))
+
 (rum/defcs result-table < rum/reactive
   (rum/local false ::select?)
   (rum/local false ::mouse-down?)
-  [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text]
+  [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text inline]
   (when current-block
     (let [select? (get state ::select?)
           *mouse-down? (::mouse-down? state)
@@ -168,53 +181,65 @@
           ;; as user needs to know if there result is sorted
           sort-state (get-sort-state current-block)
           sort-result (sort-result result (assoc sort-state :page? page?))
-          property-separated-by-commas? (partial text/separated-by-commas? (state/get-config))]
-      [:div.overflow-x-auto {:on-mouse-down (fn [e] (.stopPropagation e))
-                             :style {:width "100%"}
-                             :class (when-not page? "query-table")}
-       [:table.table-auto
-        [:thead
-         [:tr.cursor
-          (for [column columns]
-            (let [title (if (and (= column :clock-time) (integer? clock-time-total))
-                             (util/format "clock-time(total: %s)" (clock/seconds->days:hours:minutes:seconds
-                                                                   clock-time-total))
-                             (name column))]
-              (sortable-title title column sort-state (:block/uuid current-block))))]]
-        [:tbody
-         (for [row sort-result]
-           (let [format (:block/format row)]
+          property-separated-by-commas? (partial text/separated-by-commas? (state/get-config))
+          table-version (get-shui-component-version :table config)
+          result-as-text (for [row sort-result]
+                           (for [column columns] 
+                             (build-column-text row column)))
+          render-column-value (fn [row-format cell-format value]
+                                (cond 
+                                  ;; elements should be rendered as they are provided
+                                  (= :element cell-format) value 
+                                  ;; collections are treated as a comma separated list of page-cps
+                                  (coll? value) (->> (map #(page-cp {} {:block/name %}) value)
+                                                     (interpose [:span ", "]))
+                                  ;; boolean values need to first be stringified
+                                  (boolean? value) (str value) 
+                                  ;; string values will attempt to be rendered as pages, falling back to 
+                                  ;; inline-text when no page entity is found
+                                  (string? value) (if-let [page (db/entity [:block/name (util/page-name-sanity-lc value)])]
+                                                    (page-cp {} page) 
+                                                    (inline-text row-format value))
+                                  ;; anything else should just be rendered as provided
+                                  :else value))]
+                      
+      (case table-version
+        2 (shui/table-v2 {:data (conj [[columns]] result-as-text)} 
+                         (make-shui-context config inline))
+        1 [:div.overflow-x-auto {:on-mouse-down (fn [e] (.stopPropagation e))
+                                 :style {:width "100%"}
+                                 :class (when-not page? "query-table")}
+           [:table.table-auto
+            [:thead
              [:tr.cursor
               (for [column columns]
-                (let [value (build-column-value row
-                                                column
-                                                {:page? page?
-                                                 :->elem ->elem
-                                                 :map-inline map-inline
-                                                 :config config
-                                                 :comma-separated-property? (property-separated-by-commas? column)})]
-                  [:td.whitespace-nowrap {:on-mouse-down (fn []
-                                                           (reset! *mouse-down? true)
-                                                           (reset! select? false))
-                                          :on-mouse-move (fn [] (reset! select? true))
-                                          :on-mouse-up (fn []
-                                                         (when (and @*mouse-down? (not @select?))
-                                                           (state/sidebar-add-block!
-                                                            (state/get-current-repo)
-                                                            (:db/id row)
-                                                            :block-ref)
-                                                           (reset! *mouse-down? false)))}
-                   (when value
-                     (if (= :element (first value))
-                       (second value)
-                       (let [value (second value)]
-                         (if (coll? value)
-                           (let [vals (for [row value]
-                                        (page-cp {} {:block/name row}))]
-                             (interpose [:span ", "] vals))
-                           (cond
-                             (boolean? value) (str value)
-                             (string? value) (if-let [page (db/entity [:block/name (util/page-name-sanity-lc value)])]
-                                               (page-cp {} page)
-                                               (inline-text format value))
-                             :else value)))))]))]))]]])))
+                (let [title (if (and (= column :clock-time) (integer? clock-time-total))
+                              (util/format "clock-time(total: %s)" (clock/seconds->days:hours:minutes:seconds
+                                                                    clock-time-total))
+                              (name column))]
+                  (sortable-title title column sort-state (:block/uuid current-block))))]]
+            [:tbody
+             (for [row sort-result]
+               (let [format (:block/format row)]
+                 [:tr.cursor
+                  (for [column columns]
+                    (let [value (build-column-value row
+                                                    column
+                                                    {:page? page?
+                                                     :->elem ->elem
+                                                     :map-inline map-inline
+                                                     :config config
+                                                     :comma-separated-property? (property-separated-by-commas? column)})]
+                      [:td.whitespace-nowrap {:on-mouse-down (fn []
+                                                               (reset! *mouse-down? true)
+                                                               (reset! select? false))
+                                              :on-mouse-move (fn [] (reset! select? true))
+                                              :on-mouse-up (fn []
+                                                             (when (and @*mouse-down? (not @select?))
+                                                               (state/sidebar-add-block!
+                                                                (state/get-current-repo)
+                                                                (:db/id row)
+                                                                :block-ref)
+                                                               (reset! *mouse-down? false)))}
+                       (when value
+                         (apply render-column-value format value))]))]))]]]))))

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

@@ -836,7 +836,7 @@
                ;;   [:assets "assets" (t :settings-page/tab-assets) (ui/icon "box")])
 
                [:advanced "advanced" (t :settings-page/tab-advanced) (ui/icon "bulb")]
-               [:features "features" (t :settings-page/tab-features) (ui/icon "square-asterisk")]
+               [:features "features" (t :settings-page/tab-features) (ui/icon "app-feature")]
 
                (when plugins-of-settings
                  [:plugins-setting "plugins" (t :settings-of-plugins) (ui/icon "puzzle")])]]

+ 6 - 1
src/main/frontend/db/model.cljs

@@ -770,7 +770,11 @@ independent of format as format specific heading characters are stripped"
                                         :include-start? true
                                         :scoped-block-id scoped-block-id}))
 
-      (contains? #{:save-block :delete-blocks} outliner-op)
+      (and (= :delete-blocks outliner-op)
+           (<= (count @result) initial-blocks-length)) ; load more blocks
+      nil
+
+      (= :save-block outliner-op)
       @result
 
       (contains? #{:insert-blocks :collapse-expand-blocks :move-blocks} outliner-op)
@@ -845,6 +849,7 @@ independent of format as format specific heading characters are stripped"
                                                         (db-utils/pull repo-url pull-keys id))) block-eids)
                                              (db-utils/pull-many repo-url pull-keys block-eids))
                                     blocks (remove (fn [b] (nil? (:block/content b))) blocks)]
+
                                 (map (fn [b] (assoc b :block/page bare-page-map)) blocks)))}
                  nil)
         react)))))

+ 15 - 1
src/main/frontend/dicts.cljc

@@ -371,6 +371,8 @@
 
         :file-sync/other-user-graph "Current local graph is bound to other user's remote graph. So can't start syncing."
         :file-sync/graph-deleted "The current remote graph has been deleted"
+        :file-sync/rsapi-cannot-upload-err "Unable to start synchronization, please check if the local time is correct."
+
 
         :notification/clear-all "Clear all"}
 
@@ -763,7 +765,7 @@
         :right-side-bar/history "(Dev) Verlauf rückgängig machen / wiederherstellen"
         :select/default-select-multiple "Ein oder mehrere auswählen"
         :settings-page/auto-expand-block-refs "Automatisch beim Heranzoomen Blockreferenzen erweitern..."
-        
+
         :whiteboard/link-whiteboard-or-block "Whiteboard/Seite/Block verknüpfen"}
    :nl {
         :all-files "Alle bestanden"
@@ -1683,6 +1685,7 @@
 
            :file-sync/other-user-graph "当前本地图谱绑定在其他用户的远程图谱上。因此无法启动同步。"
            :file-sync/graph-deleted "当前远程图谱已经删除"
+           :file-sync/rsapi-cannot-upload-err "无法同步,请检查本机时间是否准确"
 
            :notification/clear-all "清除全部通知"}
 
@@ -2919,6 +2922,7 @@
            :settings-page/spell-checker "Verificador ortográfico"
            :settings-page/disable-sentry "Enviar dados de utilização e diagnósticos para Logseq"
            :settings-page/preferred-outdenting "Ativar dedentação lógica"
+           :settings-page/auto-expand-block-refs "Expandir as referências de bloco automaticamente ao aumentar o zoom"
            :settings-page/custom-date-format "Formato de data preferido"
            :settings-page/preferred-file-format "Formato de Arquivo preferido"
            :settings-page/preferred-workflow "Fluxo de trabalho preferido"
@@ -3246,6 +3250,10 @@
            :left-side-bar/new-whiteboard "Novo quadro branco"
            :left-side-bar/nav-favorites "Favoritos"
            :left-side-bar/nav-recent-pages "Recente"
+           :page/something-went-wrong "Algo deu errado"
+           :page/logseq-is-having-a-problem "Logseq está tendo um problema. Para tentar colocá-lo de volta em um estado de funcionamento, por favor tente os seguintes passos seguros em ordem:"
+           :page/step "Passo {1}"
+           :page/try "Tentar"
            :page/presentation-mode "Modo de apresentação"
            :page/delete-confirmation "Tem a certeza de que quer apagar esta página e o respetivo ficheiro?"
            :page/open-in-finder "Abrir em pasta"
@@ -3314,8 +3322,12 @@
            :color/pink "Rosa"
            :editor/copy "Copiar"
            :editor/cut "Cortar"
+           :content/copy-export-as "Copiar / Exportar como.."
+           :content/copy-block-url "Copiar URL do bloco"
            :content/copy-block-ref "Copiar referência do bloco"
            :content/copy-block-emebed "Copiar bloco para incorporar"
+           :content/copy-ref "Copiar esta referência"
+           :content/delete-ref "Apagar esta referência"
            :content/open-in-sidebar "Abrir na barra lateral"
            :content/click-to-edit "Clicar para editar"
            :settings-page/git-confirm "É necessário reiniciar a aplicação após atualizar as definições do Git."
@@ -3499,12 +3511,14 @@
 
            :command-palette/prompt "Introduza um comando"
            :select/default-prompt "Selecione um"
+           :select/default-select-multiple "Selecione um ou vários"
            :select.graph/prompt "Selecione um grafo"
            :select.graph/empty-placeholder-description "Sem grafos correspondentes. Quer adicionar outro?"
            :select.graph/add-graph "Sim, adicionar outro grafo"
 
            :file-sync/other-user-graph "O grafo local atual está ligado ao grafo remoto de outro utilizador. Portanto, a sincronização não pode ser iniciada."
            :file-sync/graph-deleted "O grafo remoto atual foi apagado"
+           :file-sync/rsapi-cannot-upload-err "Não foi possível iniciar a sincronização, verifique se a hora local está correta."
 
            :notification/clear-all "Limpar tudo"}
 

+ 0 - 2
src/main/frontend/format/block.cljs

@@ -9,7 +9,6 @@
             [frontend.handler.notification :as notification]
             [frontend.state :as state]
             [logseq.graph-parser.block :as gp-block]
-            [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [lambdaisland.glogi :as log]))
@@ -23,7 +22,6 @@ and handles unexpected failure."
     (gp-block/extract-blocks blocks content with-id? format
                              {:user-config (state/get-config)
                               :block-pattern (config/get-block-pattern format)
-                              :supported-formats (gp-config/supported-formats)
                               :db (db/get-db (state/get-current-repo))
                               :date-formatter (state/get-date-formatter)
                               :page-name page-name})

+ 10 - 1
src/main/frontend/fs/sync.cljs

@@ -1680,6 +1680,10 @@
   [r]
   (some->> (ex-cause r) str (re-find #"graph-not-exist")))
 
+(defn- stop-sync-by-rsapi-response?
+  [r]
+  (some->> (ex-cause r) str (re-find #"Request is not yet valid")))
+
 
 ;; type = "change" | "add" | "unlink"
 (deftype FileChangeEvent [type dir path stat checksum]
@@ -2594,10 +2598,15 @@
               (do (println :graph-has-been-deleted r*)
                   {:graph-has-been-deleted true})
 
+              (stop-sync-by-rsapi-response? r*)
+              (do (println :stop-sync-caused-by-rsapi-err-response r*)
+                  (notification/show! (t :file-sync/rsapi-cannot-upload-err) :warning false)
+                  {:stop true})
+
               paused?
               {:pause true}
 
-              succ?                   ; succ
+              succ?                     ; succ
               (do
                 (println "sync-local->remote! update txid" r*)
                 ;; persist txid

+ 0 - 2
src/main/frontend/handler/common/file.cljs

@@ -5,7 +5,6 @@
             [frontend.db :as db]
             [logseq.graph-parser :as graph-parser]
             [logseq.graph-parser.util :as gp-util]
-            [logseq.graph-parser.config :as gp-config]
             [frontend.fs.diff-merge :as diff-merge]
             [frontend.fs :as fs]
             [frontend.context.i18n :refer [t]]
@@ -107,7 +106,6 @@
                                            {:user-config (state/get-config)
                                             :date-formatter (state/get-date-formatter)
                                             :block-pattern (config/get-block-pattern (gp-util/get-format file-path))
-                                            :supported-formats (gp-config/supported-formats)
                                             :filename-format (state/get-filename-format repo-url)}
                                            ;; To avoid skipping the `:or` bounds for keyword destructuring
                                            (when (some? extracted-block-ids) {:extracted-block-ids extracted-block-ids})

+ 9 - 8
src/main/frontend/handler/editor.cljs

@@ -426,7 +426,7 @@
                    (not has-children?))]
     (outliner-tx/transact!
      {:outliner-op :insert-blocks}
-      (save-current-block! {:current-block current-block})
+     (save-current-block! {:current-block current-block})
      (outliner-core/insert-blocks! [new-block] current-block {:sibling? sibling?
                                                               :keep-uuid? keep-uuid?
                                                               :replace-empty-target? replace-empty-target?}))))
@@ -1244,8 +1244,8 @@
   [blocks]
   (outliner-tx/transact!
    {:outliner-op :save-block}
-    (doseq [[block value] blocks]
-      (save-block-if-changed! block value))))
+   (doseq [[block value] blocks]
+     (save-block-if-changed! block value))))
 
 (defn save-current-block!
   "skip-properties? if set true, when editing block is likely be properties, skip saving"
@@ -1832,8 +1832,9 @@
       (and (= content "1. ") (= last-input-char " ") input-id edit-block
            (not (own-order-number-list? edit-block)))
       (do
-        (state/pub-event! [:editor/toggle-own-number-list edit-block])
-        (state/set-edit-content! input-id ""))
+        (state/set-edit-content! input-id "")
+        (-> (p/delay 10)
+            (p/then #(state/pub-event! [:editor/toggle-own-number-list edit-block]))))
 
       (and (= last-input-char (state/get-editor-command-trigger))
            (or (re-find #"(?m)^/" (str (.-value input))) (start-of-new-word? input pos)))
@@ -2734,7 +2735,7 @@
       (outliner-tx/transact!
        {:outliner-op :move-blocks
         :real-outliner-op :indent-outdent}
-        (outliner-core/indent-outdent-blocks! [block] indent?)))
+       (outliner-core/indent-outdent-blocks! [block] indent?)))
     (state/set-editor-op! :nil)))
 
 (defn keydown-tab-handler
@@ -3177,9 +3178,9 @@
           ;; if the move is to cross block boundary, select the whole block
          (or (and (= direction :up) (cursor/textarea-cursor-rect-first-row? cursor-rect))
              (and (= direction :down) (cursor/textarea-cursor-rect-last-row? cursor-rect)))
-          (select-block-up-down direction)
+         (select-block-up-down direction)
           ;; simulate text selection
-          (cursor/select-up-down input direction anchor cursor-rect)))
+         (cursor/select-up-down input direction anchor cursor-rect)))
       (select-block-up-down direction))))
 
 (defn open-selected-block!

+ 2 - 8
src/main/frontend/handler/file_sync.cljs

@@ -16,7 +16,6 @@
             [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
             [frontend.storage :as storage]
-            [logseq.graph-parser.util :as gp-util]
             [lambdaisland.glogi :as log]))
 
 (def *beta-unavailable? (volatile! false))
@@ -168,13 +167,8 @@
   (let [file-id (:db/id (:block/file page))]
     (go
       (when-let [path (:file/path (db/entity file-id))]
-        (let [base-path (config/get-repo-dir (state/get-current-repo))
-              base-path (if (string/starts-with? base-path "file://")
-                          (gp-util/safe-decode-uri-component base-path)
-                          base-path)
-              path*     (string/replace-first (string/replace-first path base-path "") #"^/" "")
-              version-list       (:VersionList
-                                  (<! (sync/<get-remote-file-versions sync/remoteapi graph-uuid path*)))
+        (let [version-list       (:VersionList
+                                  (<! (sync/<get-remote-file-versions sync/remoteapi graph-uuid path)))
               local-version-list (<! (<list-file-local-versions page))
               all-version-list   (->> (concat version-list local-version-list)
                                       (sort-by #(or (:CreateTime %)

+ 10 - 7
src/main/frontend/handler/whiteboard.cljs

@@ -89,11 +89,12 @@
   (let [assets (js->clj-keywordize (.getCleanUpAssets app))
         new-shapes (.-shapes tl-page)
         shapes-index (map #(gobj/get % "id") new-shapes)
+        shape-id->index (zipmap shapes-index (range (.-length new-shapes)))
         upsert-shapes (->> (set/difference new-id-nonces db-id-nonces)
                            (map (fn [{:keys [id]}]
                                   (-> (.-serialized ^js (.getShapeById tl-page id))
                                       js->clj-keywordize
-                                      (assoc :index (.indexOf shapes-index id)))))
+                                      (assoc :index (get shape-id->index id)))))
                            (set))
         old-ids (set (map :id db-id-nonces))
         new-ids (set (map :id new-id-nonces))
@@ -134,13 +135,15 @@
 (defn transact-tldr-delta! [page-name ^js app replace?]
   (let [tl-page ^js (second (first (.-pages app)))
         shapes (.-shapes ^js tl-page)
-        shapes-index (map #(gobj/get % "id") shapes)
-        new-id-nonces (set (map (fn [shape]
+        page-block (model/get-page page-name)
+        prev-shapes-index (get-in page-block [:block/properties :logseq.tldraw.page :shapes-index])
+        shape-id->prev-index (zipmap prev-shapes-index (range (count prev-shapes-index)))
+        new-id-nonces (set (map-indexed (fn [idx shape]
                                   (let [id (.-id shape)]
-                                   {:id id
-                                    :nonce (if (= shape.id (.indexOf shapes-index id))
-                                             (.-nonce shape)
-                                             (.getTime (js/Date.)))})) shapes))
+                                    {:id id
+                                     :nonce (if (= idx (get shape-id->prev-index id))
+                                              (.-nonce shape)
+                                              (js/Date.now))})) shapes))
         repo (state/get-current-repo)
         db-id-nonces (or
                       (get-in @*last-shapes-nonce [repo page-name])

+ 12 - 4
src/main/frontend/modules/outliner/core.cljs

@@ -16,8 +16,7 @@
             [logseq.graph-parser.util :as gp-util]
             [cljs.spec.alpha :as s]))
 
-(s/def ::block-map (s/keys :req [:db/id]
-                           :opt [:block/page :block/left :block/parent]))
+(s/def ::block-map (s/keys :opt [:db/id :block/uuid :block/page :block/left :block/parent]))
 
 (s/def ::block-map-or-entity (s/or :entity de/entity?
                                    :map ::block-map))
@@ -511,6 +510,13 @@
                          (dissoc :db/id)))))
                  blocks)))
 
+(defn- get-target-block
+  [target-block]
+  (if (:db/id target-block)
+    (db/pull (:db/id target-block))
+    (when (:block/uuid target-block)
+      (db/pull [:block/uuid (:block/uuid target-block)]))))
+
 (defn insert-blocks
   "Insert blocks as children (or siblings) of target-node.
   Args:
@@ -528,7 +534,7 @@
   [blocks target-block {:keys [sibling? keep-uuid? outliner-op replace-empty-target?] :as opts}]
   {:pre [(seq blocks)
          (s/valid? ::block-map-or-entity target-block)]}
-  (let [target-block' (db/pull (:db/id target-block))
+  (let [target-block' (get-target-block target-block)
         _ (assert (some? target-block') (str "Invalid target: " target-block))
         sibling? (if (page-block? target-block') false sibling?)
         move? (contains? #{:move-blocks :move-blocks-up-down :indent-outdent-blocks} outliner-op)
@@ -714,7 +720,9 @@
   [blocks target-block {:keys [sibling? outliner-op]}]
   [:pre [(seq blocks)
          (s/valid? ::block-map-or-entity target-block)]]
-  (let [non-consecutive-blocks? (seq (db-model/get-non-consecutive-blocks blocks))
+  (let [target-block (get-target-block target-block)
+        _ (assert (some? target-block) (str "Invalid target: " target-block))
+        non-consecutive-blocks? (seq (db-model/get-non-consecutive-blocks blocks))
         original-position? (move-to-original-position? blocks target-block sibling? non-consecutive-blocks?)]
     (when (and (not (contains? (set (map :db/id blocks)) (:db/id target-block)))
                (not original-position?))

+ 7 - 0
src/main/frontend/modules/outliner/datascript.cljc

@@ -3,6 +3,7 @@
   #?(:cljs (:require-macros [frontend.modules.outliner.datascript]))
   #?(:cljs (:require [datascript.core :as d]
                      [frontend.db.conn :as conn]
+                     [frontend.db :as db]
                      [frontend.modules.outliner.pipeline :as pipelines]
                      [frontend.modules.editor.undo-redo :as undo-redo]
                      [frontend.state :as state]
@@ -73,6 +74,12 @@
                  rs (d/transact! conn txs (assoc opts :outliner/transact? true))
                  tx-id (get-tx-id rs)]
              (swap! state/state assoc-in [:history/tx->editor-cursor tx-id] before-editor-cursor)
+
+             ;; update the current edit block to include full information
+             (when-let [block (state/get-edit-block)]
+               (when (and (:block/uuid block) (not (:db/id block)))
+                 (state/set-state! :editor/block (db/pull [:block/uuid (:block/uuid block)]))))
+
              (when true                 ; TODO: add debug flag
                (let [eids (distinct (mapv first (:tx-data rs)))
                      left&parent-list (->>

+ 6 - 6
src/main/frontend/page.cljs

@@ -52,11 +52,11 @@
            [:div.flex.flex-col.items-start
             [:div.text-2xs.font-bold.uppercase.toned-down (t :page/step "1")]
             [:div [:span.highlighted.font-bold "Rebuild"] [:span.toned-down " search index"]]]
-             [:div
-              (ui/button (t :page/try)
-                         :small? true
-                         :on-click (fn []
-                                     (search-handler/rebuild-indices! true)))]]
+           [:div
+            (ui/button (t :page/try)
+                       :small? true
+                       :on-click (fn []
+                                   (search-handler/rebuild-indices! true)))]]
           [:div.flex.flex-row.justify-between.align-items.mb-2.items-center.separator-top.py-4
            [:div.flex.flex-col.items-start
             [:div.text-2xs.font-bold.uppercase.toned-down (t :page/step "2")]
@@ -92,7 +92,7 @@
                    (ui/inject-dynamic-style-node!)
                    (quick-tour/init)
                    (plugin-handler/host-mounted!)
-                   (assoc state ::teardown (setup-fns!) ))
+                   (assoc state ::teardown (setup-fns!)))
    :will-unmount (fn [state]
                    (when-let [teardown (::teardown state)]
                      (teardown)))}

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

@@ -92,7 +92,7 @@
       (fn []
          (rum/set-ref! *mounted true)
          #(rum/set-ref! *mounted false))
-       [])
+      [])
     #(rum/deref *mounted)))
 
 (defn use-bounding-client-rect

+ 25 - 0
src/main/frontend/shui.cljs

@@ -0,0 +1,25 @@
+(ns frontend.shui
+  "Glue between frontend code and deps/shui for convenience"
+  (:require 
+    [frontend.date :refer [int->local-time-2]]
+    [frontend.state :as state]
+    [logseq.shui.context :refer [make-context]]))
+
+(def default-versions {:logseq.table.version 1})
+
+(defn get-shui-component-version 
+  "Returns the version of the shui component, checking first 
+  the block properties, then the global config, then the defaults."
+  [component-name block-config]
+  (let [version-key (keyword (str "logseq." (name component-name) ".version"))]
+    (js/parseFloat
+      (or (get-in block-config [:block :block/properties version-key])
+          (get-in (state/get-config) [version-key])
+          (get-in default-versions [version-key])
+          1))))
+
+(defn make-shui-context [block-config inline]
+  (make-context {:block-config block-config 
+                 :app-config (state/get-config) 
+                 :inline inline 
+                 :int->local-time-2 int->local-time-2}))

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

@@ -259,9 +259,9 @@
      ;;                :file-sync/progress {}
      ;;                :file-sync/start-time {}
      ;;                :file-sync/last-synced-at {}}
-     :file-sync/graph-state                 {:current-graph-uuid nil
+     :file-sync/graph-state                 {:current-graph-uuid nil}
                                              ;; graph-uuid -> ...
-                                             }
+                                             
      :user/info                             {:UserGroups (storage/get :user-groups)}
      :encryption/graph-parsing?             false
 

+ 24 - 24
src/main/frontend/ui.cljs

@@ -730,26 +730,26 @@
   [state {:keys [on-mouse-down header title-trigger? collapsed?]}]
   (let [control? (get state ::control?)]
     [:div.content
-    [:div.flex-1.flex-row.foldable-title (cond->
-                                           {:on-mouse-over #(reset! control? true)
-                                            :on-mouse-out  #(reset! control? false)}
-                                           title-trigger?
-                                           (assoc :on-mouse-down on-mouse-down
-                                                  :class "cursor"))
-     [:div.flex.flex-row.items-center
-      (when-not (mobile-util/native-platform?)
-        [:a.block-control.opacity-50.hover:opacity-100.mr-2
-         (cond->
-           {:style    {:width       14
-                       :height      16
-                       :margin-left -30}}
-           (not title-trigger?)
-           (assoc :on-mouse-down on-mouse-down))
-         [:span {:class (if (or @control? @collapsed?) "control-show cursor-pointer" "control-hide")}
-          (rotating-arrow @collapsed?)]])
-      (if (fn? header)
-        (header @collapsed?)
-        header)]]]))
+     [:div.flex-1.flex-row.foldable-title (cond->
+                                            {:on-mouse-over #(reset! control? true)
+                                             :on-mouse-out  #(reset! control? false)}
+                                            title-trigger?
+                                            (assoc :on-mouse-down on-mouse-down
+                                                   :class "cursor"))
+      [:div.flex.flex-row.items-center
+       (when-not (mobile-util/native-platform?)
+         [:a.block-control.opacity-50.hover:opacity-100.mr-2
+          (cond->
+            {:style    {:width       14
+                        :height      16
+                        :margin-left -30}}
+            (not title-trigger?)
+            (assoc :on-mouse-down on-mouse-down))
+          [:span {:class (if (or @control? @collapsed?) "control-show cursor-pointer" "control-hide")}
+           (rotating-arrow @collapsed?)]])
+       (if (fn? header)
+         (header @collapsed?)
+         header)]]]))
 
 (rum/defcs foldable < db-mixins/query rum/reactive
   (rum/local false ::collapsed?)
@@ -852,10 +852,10 @@
       [:option (cond->
                 {:key   label
                  :value (or value label)} ;; NOTE: value might be an empty string, `or` is safe here
-                 disabled
-                 (assoc :disabled disabled)
-                 selected
-                 (assoc :selected selected))
+                disabled
+                (assoc :disabled disabled)
+                selected
+                (assoc :selected selected))
        label])]))
 
 (rum/defc radio-list

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

@@ -226,7 +226,7 @@
 
 (defn- move-cursor-up-down
   [input direction]
-    (move-cursor-to input (next-cursor-pos-up-down direction (get-caret-pos input))))
+  (move-cursor-to input (next-cursor-pos-up-down direction (get-caret-pos input))))
 
 (defn move-cursor-up [input]
   (move-cursor-up-down input :up))

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

@@ -1,3 +1,3 @@
 (ns ^:no-doc frontend.version)
 
-(defonce version "0.9.5")
+(defonce version "0.9.6")

+ 4 - 20
src/main/logseq/api.cljs

@@ -43,7 +43,8 @@
             [frontend.handler.shell :as shell]
             [frontend.modules.layout.core]
             [frontend.handler.code :as code-handler]
-            [frontend.handler.search :as search-handler]))
+            [frontend.handler.search :as search-handler]
+            [logseq.api.block :as api-block]))
 
 ;; Alert: this namespace shouldn't invoke any reactive queries
 
@@ -676,24 +677,7 @@
           target-block (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error target-block-uuid))]
       (editor-dnd-handler/move-blocks nil [src-block] target-block move-to) nil)))
 
-(def ^:export get_block
-  (fn [id-or-uuid ^js opts]
-    (when-let [block (cond
-                       (number? id-or-uuid) (db-utils/pull id-or-uuid)
-                       (string? id-or-uuid) (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error id-or-uuid)))]
-      (when-not (contains? block :block/name)
-        (when-let [uuid (:block/uuid block)]
-          (let [{:keys [includeChildren]} (bean/->clj opts)
-                repo  (state/get-current-repo)
-                block (if includeChildren
-                        ;; nested children results
-                        (first (outliner-tree/blocks->vec-tree
-                                (db-model/get-block-and-children repo uuid) uuid))
-                        ;; attached shallow children
-                        (assoc block :block/children
-                               (map #(list :uuid (:block/uuid %))
-                                 (db/get-block-immediate-children repo uuid))))]
-            (bean/->js (sdk-utils/normalize-keyword-for-json block))))))))
+(def ^:export get_block api-block/get_block)
 
 (def ^:export get_current_block
   (fn [^js opts]
@@ -703,7 +687,7 @@
                                 (gdom/getElement (state/get-editing-block-dom-id)))
                             (.getAttribute "blockid")
                             (db-model/get-block-by-uuid)))]
-      (get_block (:db/id block) opts))))
+      (get_block (:block/uuid block) opts))))
 
 (def ^:export get_previous_sibling_block
   (fn [block-uuid]

+ 28 - 0
src/main/logseq/api/block.cljs

@@ -0,0 +1,28 @@
+(ns logseq.api.block
+  "Block related apis"
+  (:require [frontend.db.model :as db-model]
+            [frontend.db.utils :as db-utils]
+            [cljs-bean.core :as bean]
+            [frontend.state :as state]
+            [frontend.modules.outliner.tree :as outliner-tree]
+            [frontend.db :as db]
+            [logseq.sdk.utils :as sdk-utils]))
+
+(defn get_block
+  [id-or-uuid ^js opts]
+  (when-let [block (if (number? id-or-uuid)
+                     (db-utils/pull id-or-uuid)
+                     (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error id-or-uuid)))]
+    (when-not (contains? block :block/name)
+      (when-let [uuid (:block/uuid block)]
+        (let [{:keys [includeChildren]} (bean/->clj opts)
+              repo  (state/get-current-repo)
+              block (if includeChildren
+                      ;; nested children results
+                      (first (outliner-tree/blocks->vec-tree
+                              (db-model/get-block-and-children repo uuid) uuid))
+                      ;; attached shallow children
+                      (assoc block :block/children
+                             (map #(list :uuid (:block/uuid %))
+                               (db/get-block-immediate-children repo uuid))))]
+          (bean/->js (sdk-utils/normalize-keyword-for-json block)))))))

+ 3 - 2
src/test/frontend/fs_test.cljs

@@ -2,6 +2,7 @@
   (:require [clojure.test :refer [is use-fixtures]]
             [frontend.test.fixtures :as fixtures]
             [frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]]
+            [frontend.test.node-helper :as test-node-helper]
             [frontend.fs :as fs]
             [promesa.core :as p]
             ["fs" :as fs-node]
@@ -11,7 +12,7 @@
 
 (deftest-async create-if-not-exists-creates-correctly
   ;; dir needs to be an absolute path for fn to work correctly
-  (let [dir (node-path/resolve (test-helper/create-tmp-dir))
+  (let [dir (node-path/resolve (test-node-helper/create-tmp-dir))
         some-file (node-path/join dir "something.txt")]
 
     (->
@@ -29,7 +30,7 @@
         (fs-node/rmdirSync dir))))))
 
 (deftest-async create-if-not-exists-does-not-create-correctly
-  (let [dir (node-path/resolve (test-helper/create-tmp-dir))
+  (let [dir (node-path/resolve (test-node-helper/create-tmp-dir))
         some-file (node-path/join dir "something.txt")]
     (fs-node/writeFileSync some-file "OLD")
 

+ 2 - 1
src/test/frontend/handler/plugin_config_test.cljs

@@ -1,6 +1,7 @@
 (ns frontend.handler.plugin-config-test
   (:require [clojure.test :refer [is use-fixtures testing deftest]]
             [frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]]
+            [frontend.test.node-helper :as test-node-helper]
             [frontend.test.fixtures :as fixtures]
             [frontend.handler.plugin-config :as plugin-config-handler]
             [frontend.handler.global-config :as global-config-handler]
@@ -17,7 +18,7 @@
 
 (defn- create-global-config-dir
   []
-  (let [dir (test-helper/create-tmp-dir "config")
+  (let [dir (test-node-helper/create-tmp-dir "config")
         root-dir (node-path/dirname dir)]
     (reset! global-config-handler/root-dir root-dir)
     dir))

+ 1 - 1
src/test/frontend/handler/repo_conversion_test.cljs

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

+ 1 - 16
src/test/frontend/test/helper.cljs

@@ -1,9 +1,7 @@
 (ns frontend.test.helper
   "Common helper fns for tests"
   (:require [frontend.handler.repo :as repo-handler]
-            [frontend.db.conn :as conn]
-            ["path" :as node-path]
-            ["fs" :as fs-node]))
+            [frontend.db.conn :as conn]))
 
 (defonce test-db "test-db")
 
@@ -24,16 +22,3 @@ This can be called in synchronous contexts as no async fns should be invoked"
    files
    ;; Set :refresh? to avoid creating default files in after-parse
    {:re-render? false :verbose false :refresh? true}))
-
-(defn create-tmp-dir
-  "Creates a temporary directory under tmp/. If a subdir is given, creates an
-  additional subdirectory under the newly created temp directory."
-  ([] (create-tmp-dir nil))
-  ([subdir]
-   (when-not (fs-node/existsSync "tmp") (fs-node/mkdirSync "tmp"))
-   (let [dir (fs-node/mkdtempSync (node-path/join "tmp" "unit-test-"))]
-     (if subdir
-       (do
-         (fs-node/mkdirSync (node-path/join dir subdir))
-         (node-path/join dir subdir))
-       dir))))

+ 17 - 0
src/test/frontend/test/node_helper.cljs

@@ -0,0 +1,17 @@
+(ns frontend.test.node-helper
+  "Common helper fns for node tests"
+  (:require ["path" :as node-path]
+            ["fs" :as fs-node]))
+
+(defn create-tmp-dir
+  "Creates a temporary directory under tmp/. If a subdir is given, creates an
+  additional subdirectory under the newly created temp directory."
+  ([] (create-tmp-dir nil))
+  ([subdir]
+   (when-not (fs-node/existsSync "tmp") (fs-node/mkdirSync "tmp"))
+   (let [dir (fs-node/mkdtempSync (node-path/join "tmp" "unit-test-"))]
+     (if subdir
+       (do
+         (fs-node/mkdirSync (node-path/join dir subdir))
+         (node-path/join dir subdir))
+       dir))))

+ 40 - 0
src/test/logseq/api_test.cljs

@@ -0,0 +1,40 @@
+(ns logseq.api-test
+  (:require [cljs.test :refer [use-fixtures deftest is]]
+            [frontend.test.helper :as test-helper]
+            [frontend.db :as db]
+            [logseq.api.block :as api-block]
+            [frontend.state :as state]
+            [cljs-bean.core :as bean]))
+
+(use-fixtures :each {:before test-helper/start-test-db!
+                     :after test-helper/destroy-test-db!})
+
+(deftest get-block
+  (with-redefs [state/get-current-repo (constantly test-helper/test-db)]
+    (db/transact! test-helper/test-db
+      [{:db/id 10000
+        :block/uuid #uuid "4406f839-6410-43b5-87db-25e9b8f54cc0"
+        :block/content "1"}
+       {:db/id 10001
+        :block/uuid #uuid "d9b7b45f-267f-4794-9569-f43d1ce77172"
+        :block/content "2"}
+       {:db/id 10002
+        :block/uuid #uuid "adae3006-f03e-4814-a1f5-f17f15b86556"
+        :block/parent 10001
+        :block/left 10001
+        :block/content "3"}
+       {:db/id 10003
+        :block/uuid #uuid "0c3053c3-2dab-4769-badd-14ce16d8ba8d"
+        :block/parent 10002
+        :block/left 10002
+        :block/content "4"}])
+
+    (is (= (:content (bean/->clj (api-block/get_block 10000 #js {}))) "1"))
+    (is (= (:content (bean/->clj (api-block/get_block "d9b7b45f-267f-4794-9569-f43d1ce77172" #js {}))) "2"))
+    (is (= (:content (bean/->clj (api-block/get_block #uuid "d9b7b45f-267f-4794-9569-f43d1ce77172" #js {}))) "2"))
+    (is (= {:id 10001, :content "2", :uuid "d9b7b45f-267f-4794-9569-f43d1ce77172", :children [["uuid" "adae3006-f03e-4814-a1f5-f17f15b86556"]]}
+           (bean/->clj (api-block/get_block 10001 #js {:includeChildren false}))))
+    (is (= {:content "2", :uuid "d9b7b45f-267f-4794-9569-f43d1ce77172", :id 10001, :children [{:content "3", :left {:id 10001}, :parent {:id 10001}, :uuid "adae3006-f03e-4814-a1f5-f17f15b86556", :id 10002, :level 1, :children [{:content "4", :left {:id 10002}, :parent {:id 10002}, :uuid "0c3053c3-2dab-4769-badd-14ce16d8ba8d", :id 10003, :level 2, :children []}]}]}
+           (bean/->clj (api-block/get_block 10001 #js {:includeChildren true}))))))
+
+#_(cljs.test/run-tests)

+ 226 - 16
tailwind.config.js

@@ -1,4 +1,29 @@
+const plugin = require('tailwindcss/plugin')
 const colors = require('tailwindcss/colors')
+const radix = require('@radix-ui/colors')
+
+const gradientColors = {
+  tomato:  ["amber",     "orange",    "tomato",      "red",         "crimson"],
+  red:     ["orange",    "tomato",    "red",         "crimson",     "pink"], 
+  crimson: ["tomato",    "red",       "crimson",     "pink",        "plum"],
+  pink:    ["red",       "crimson",   "pink",        "plum",        "purple"],
+  plum:    ["crimson",   "pink",      "plum",        "purple",      "violet"], 
+  purple:  ["pink",      "plum",      "purple",      "violet",      "indigo"],
+  violet:  ["plum",      "purple",    "violet",      "indigo",      "blue"],  
+  indigo:  ["purple",    "violet",    "indigo",      "blue",        "cyan"],
+  blue:    ["violet",    "indigo",    "blue",        "cyan",        "teal"],
+  // sky:     ["indigo",    "blue",      "sky",         "cyan",        "teal"],
+  cyan:    ["indigo",    "blue",      "cyan",        "teal",        "green"],
+  teal:    ["blue",      "cyan",      "teal",        "green",       "grass"],
+  // mint:    ["cyan",      "teal",      "mint",        "green",       "grass"],
+  green:   ["cyan",      "teal",      "green",       "grass",       "amber"],
+  grass:   ["teal",      "green",     "grass",       "amber",       "orange"],
+  // lime:    ["green",     "grass",     "lime",        "yellow",      "amber"],
+  // yellow:  ["grass",     "lime",      "yellow",      "amber",       "orange"],
+  amber:   ["green",     "grass",     "amber",       "orange",      "tomato"],
+  orange:  ["grass",     "amber",     "orange",      "tomato",      "red"],
+  // brown:   ["green",     "grass",     "brown",       "tomato",       "red"],
+}
 
 function exposeColorsToCssVars ({ addBase, theme }) {
   function extractColorVars (colorObj, colorGroup = '') {
@@ -19,28 +44,160 @@ function exposeColorsToCssVars ({ addBase, theme }) {
   })
 }
 
+function buildColor(color, custom) {
+  const base = custom || colors[color] || {}
+
+  for (const [xName, xValue] of Object.entries(radix[color] || {})) {
+    const regexResult = xName.match(/\d+$/) 
+    if (!regexResult) { continue; } 
+    const xStep = regexResult[0]
+    base[xStep] = xValue
+  }
+
+  return base
+}
+
+// this will allow us to use gradient color functions in the ui:
+// grad-bg-tomato-3 OR grad-bg-tomato-3-alpha
+// it will also loop through all 5 color stops, unless the stops are specified 
+// grad-bg-stops-3
+// this will have a default repeating gradient at a step that can be configured with 
+// grad-bg-cycle-32 
+const addGradientColors = plugin(({ addBase, addComponents, addUtilities, config, ___theme }) => {
+  const dark = getDarkSelector(config)
+
+  addUtilities({
+    ['.grad-bg-stops-3']: {
+      '--grad-bg-stops': "var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d)",
+    },
+    ['.grad-bg-stops-5']: {
+      '--grad-bg-stops': "var(--grad-bg-stop-a), var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d), var(--grad-bg-stop-e)",
+    },
+    ['.grad-bg-cycle-12']: {
+      'background-image': 'repeatint-linear-gradient(to right, var(--grad-bg-stops))',
+    },
+  })
+
+  Object.values(gradientColors).forEach((stops, ___index) => {
+    const baseColor = stops[2]
+    const color = (scale, stopIndex = 2, suffix = "") => `--color-${stops[stopIndex]}${suffix}-${scale}`
+
+    addComponents({
+      // tailwind componnent for .grad-bg-COLOR-9
+      [`.grad-bg-${baseColor}-9`]: {
+        "--grad-bg-stop-a": `var(${color(9, 0)})`,
+        "--grad-bg-stop-b": `var(${color(9, 1)})`,
+        "--grad-bg-stop-c": `var(${color(9, 2)})`,
+        "--grad-bg-stop-d": `var(${color(9, 3)})`,
+        "--grad-bg-stop-e": `var(${color(9, 4)})`,
+        "--grad-bg-stops-default": `var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d)`,
+        "background-image": `linear-gradient(var(--grad-bg-direction, to right), var(--grad-bg-stops, var(--grad-bg-stops-default)))`,
+
+        [dark]: {
+          "--grad-bg-stop-a": `var(${color(9, 0, "dark")})`,
+          "--grad-bg-stop-b": `var(${color(9, 1, "dark")})`,
+          "--grad-bg-stop-c": `var(${color(9, 2, "dark")})`,
+          "--grad-bg-stop-d": `var(${color(9, 3, "dark")})`,
+          "--grad-bg-stop-e": `var(${color(9, 4, "dark")})`,
+        }
+      },
+      // tailwind component for .grad-bg-COLOR-9-alpha
+      [`.grad-bg-${baseColor}-9-alpha`]: {
+        "--grad-bg-stop-a": `var(${color(9, 0)})`,
+        "--grad-bg-stop-b": `var(${color(9, 1)})`,
+        "--grad-bg-stop-c": `var(${color(9, 2)})`,
+        "--grad-bg-stop-d": `var(${color(9, 3)})`,
+        "--grad-bg-stop-e": `var(${color(9, 4)})`,
+        "--grad-bg-stops-default": `var(--grad-bg-stop-b), var(--grad-bg-stop-c), var(--grad-bg-stop-d)`,
+        "background-image": `linear-gradient(var(--grad-bg-direction, to right), var(--grad-bg-stops, var(--grad-bg-stops-default)))`,
+
+        [dark]: {
+          "--grad-bg-stop-a": `var(${color(9, 0, "dark")})`,
+          "--grad-bg-stop-b": `var(${color(9, 1, "dark")})`,
+          "--grad-bg-stop-c": `var(${color(9, 2, "dark")})`,
+          "--grad-bg-stop-d": `var(${color(9, 3, "dark")})`,
+          "--grad-bg-stop-e": `var(${color(9, 4, "dark")})`,
+        }
+      },
+    })
+  })
+})
+
+function getDarkSelector(config) {
+  const darkMode = config("darkMode");
+  const prefix = config("prefix");
+
+  if (Array.isArray(darkMode)) {
+    if (darkMode.length < 2) {
+      throw new Error(
+        "To customize the dark mode selector, `darkMode` should contain two items. Documentation: https://tailwindcss.com/docs/dark-mode#customizing-the-class-name"
+      );
+    }
+
+    if (darkMode[0] !== "class") {
+      throw new Error(
+        'To customize the dark mode selector, `darkMode` should have "class" as its first item. Documentation: https://tailwindcss.com/docs/dark-mode#customizing-the-class-name'
+      );
+    }
+
+    return darkMode[1] + " &";
+  }
+
+  if (darkMode === "media") {
+    return "@media (prefers-color-scheme: dark)";
+  }
+
+  if (darkMode !== "class") {
+    throw new Error(
+      "Invalid `darkMode`. Documentation: https://tailwindcss.com/docs/dark-mode"
+    );
+  }
+
+  if (prefix) {
+    return `[class~="${prefix}dark"] &`;
+  }
+
+  return '[class~="dark"] &';
+}
+
 module.exports = {
   darkMode: 'class',
   content: [
     './src/**/*.js',
     './src/**/*.cljs',
-    './resources/**/*.html'
+    './resources/**/*.html',
+    './deps/shui/src/**/*.cljs',
   ],
   safelist: [
-    'bg-black', 'bg-white',
+    'bg-black', 'bg-white', 'capitalize-first',
     { pattern: /bg-(gray|red|yellow|green|blue|orange|indigo|rose|purple|pink)-(100|200|300|400|500|600|700|800|900)/ },
     { pattern: /text-(gray|red|yellow|green|blue|orange|indigo|rose|purple|pink)-(100|200|300|400|500|600|700|800|900)/ },
-    { pattern: /columns-([1-9]|1[0-2])|(auto|3xs|2xs|xs|sm|md|lg|xl)|([2-7]xl)/ }
+    { pattern: /columns-([1-9]|1[0-2])|(auto|3xs|2xs|xs|sm|md|lg|xl)|([2-7]xl)/ },
+    { pattern: /bg-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
+    { pattern: /shadow-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
+    { pattern: /text-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
+    { pattern: /ring-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
+    { pattern: /from-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
+    { pattern: /via-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
+    { pattern: /to-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(1|2|3|4|5|6|7|8|9|10|11|12)/ },
+    { pattern: /border-(mauve|slate|sage|olive|sand|tomato|red|crimson|pink|plum|purple|violet|indigo|blue|sky|cyan|teal|mint|green|grass|lime|yellow|amber|orange|brown)(dark)?-(4|5|6|7|8)/ },
   ],
   plugins: [
     require('@tailwindcss/forms'),
     require('@tailwindcss/typography'),
     require('@tailwindcss/aspect-ratio'),
     require('@tailwindcss/line-clamp'),
+    require('tailwind-capitalize-first-letter'),
+    addGradientColors,
     exposeColorsToCssVars
   ],
   theme: {
     extend: {
+      backgroundImage: {
+        'gradient-conic': 'conic-gradient(var(--tw-gradient-stops))',
+        'gradient-conic-bounce': 'conic-gradient(var(--tw-gradient-from), var(--tw-gradient-via), var(--tw-gradient-to), var(--tw-gradient-via), var(--tw-gradient-from))',
+        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
+      },
       fontSize: {
         '2xs': ['0.625rem', '0.875rem']
       },
@@ -64,14 +221,44 @@ module.exports = {
       }
     },
     colors: {
-      transparent: 'transparent',
-      current: 'currentColor',
+      // Tailwind colors
       black: colors.black,
+      current: 'currentColor',
+      rose: colors.rose,
+      transparent: 'transparent',
       white: colors.white,
-      gray: colors.neutral,
-      green: colors.green,
-      blue: colors.blue,
-      indigo: {
+
+      // Radix colors
+      amber: buildColor("amber"),
+      blue: buildColor("blue"),
+      bronze: buildColor("bronze"),
+      brown: buildColor("brown"),
+      crimson: buildColor("crimson"),
+      cyan: buildColor("cyan"),
+      gold: buildColor("gold"),
+      grass: buildColor("grass"),
+      green: buildColor("green"),
+      lime: buildColor("lime"),
+      mauve: buildColor("mauve"),
+      mint: buildColor("mint"),
+      olive: buildColor("olive"),
+      orange: buildColor("orange"),
+      pink: buildColor("pink"),
+      plum: buildColor("plum"),
+      purple: buildColor("purple"),
+      red: buildColor("red"),
+      sage: buildColor("sage"),
+      sand: buildColor("sand"),
+      sky: buildColor("sky"),
+      slate: buildColor("slate"),
+      teal: buildColor("teal"),
+      tomato: buildColor("tomato"),
+      violet: buildColor("violet"),
+        
+      // Custom colors
+      gray: buildColor("gray", colors.neutral),
+      yellow: buildColor("yellow", colors.amber),
+      indigo: buildColor("indigo", {
         50: '#f0f9ff',
         100: '#e0f2fe',
         200: '#bae6fd',
@@ -82,13 +269,36 @@ module.exports = {
         700: '#005b8a',
         800: '#075985',
         900: '#0c4a6e',
-      },
-      red: colors.red,
-      yellow: colors.amber,
-      orange: colors.orange,
-      rose: colors.rose,
-      purple: colors.purple,
-      pink: colors.pink
+      }),
+
+      tomatodark: buildColor("tomatoDark"),
+      reddark: buildColor("redDark"),
+      crimsondark: buildColor("crimsonDark"),
+      pinkdark: buildColor("pinkDark"),
+      plumdark: buildColor("plumDark"),
+      purpledark: buildColor("purpleDark"),
+      violetdark: buildColor("violetDark"),
+      skydark: buildColor("skyDark"),
+      indigodark: buildColor("indigoDark"),
+      bluedark: buildColor("blueDark"),
+      cyandark: buildColor("cyanDark"),
+      mintdark: buildColor("mintDark"),
+      tealdark: buildColor("tealDark"),
+      greendark: buildColor("greenDark"),
+      limedark: buildColor("limeDark"),
+      grassdark: buildColor("grassDark"),
+      yellowdark: buildColor("yellowDark"),
+      amberdark: buildColor("amberDark"),
+      orangedark: buildColor("orangeDark"),
+      browndark: buildColor("brownDark"),
+      graydark: buildColor("grayDark"),
+      mauvedark: buildColor("mauveDark"),
+      slatedark: buildColor("slateDark"),
+      sagedark: buildColor("sageDark"),
+      olivedark: buildColor("oliveDark"),
+      sanddark: buildColor("sandDark"),
+      golddark: buildColor("goldDark"),
+      bronzedark: buildColor("bronzeDark"),
     }
   }
 }

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

@@ -125,7 +125,6 @@ export class TranslatingState<
   onExit = () => {
     // Resume the history when we exit
     this.app.history.resume()
-    this.app.persist()
 
     // Reset initial data
     this.didClone = false

+ 10 - 0
yarn.lock

@@ -733,6 +733,11 @@
   resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
   integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
 
+"@radix-ui/colors@^0.1.8":
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/@radix-ui/colors/-/colors-0.1.8.tgz#b08c62536fc462a87632165fb28e9b18f9bd047e"
+  integrity sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw==
+
 "@sentry/[email protected]":
   version "6.19.7"
   resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.19.7.tgz#a40b6b72d911b5f1ed70ed3b4e7d4d4e625c0b5f"
@@ -7048,6 +7053,11 @@ table@^6.6.0:
     string-width "^4.2.3"
     strip-ansi "^6.0.1"
 
+tailwind-capitalize-first-letter@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/tailwind-capitalize-first-letter/-/tailwind-capitalize-first-letter-1.0.4.tgz#d7a07c1dda4a7555f2240d57154df394b0ee8db6"
+  integrity sha512-ZB8hBi68JI4aQ1cDUxuFWfMYTxgBvlzIdPPHSkFkMUlo7p2QlbMy0hVv/vAREAFmkUh9QfjuKQnOSbe4Gnqljg==
+
 [email protected]:
   version "3.1.8"
   resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.8.tgz#4f8520550d67a835d32f2f4021580f9fddb7b741"

部分文件因为文件数量过多而无法显示