diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index cff27b68cf5..9f69104b79d 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -146,11 +146,13 @@ jobs: ./gradlew clean ./gradlew zipApksForRelease working-directory: android + env: + LOGSEQ_SENTRY_DSN: ${{ secrets.LOGSEQ_SENTRY_DSN }} - name: Sign Android APK run: | echo ${{ secrets.ANDROID_KEYSTORE }} | base64 -d > keystore.jks - /usr/local/lib/android/sdk/build-tools/30.0.3/apksigner sign \ + /usr/local/lib/android/sdk/build-tools/33.0.0/apksigner sign \ --ks keystore.jks --ks-pass "pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" \ --in app/build/outputs/apk/release/app-release-unsigned.apk \ --out app-signed.apk diff --git a/.gitignore b/.gitignore index f631847aa46..c04b1df5ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ android/app/src/main/assets/capacitor.config.json .yarn/ .yarnrc.yml +packages/ui/.storybook/cljs deps/shui/.lsp deps/shui/.lsp-cache deps/shui/.clj-kondo diff --git a/Dockerfile b/Dockerfile index 3073f34a01b..1cef94d3456 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ ca-certificates \ apt-transport-https \ - gpg + gpg \ + build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev # install NodeJS & yarn RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - diff --git a/android/.gitignore b/android/.gitignore index 63c86fe3094..4230ebef042 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -94,3 +94,6 @@ capacitor-cordova-android-plugins # Copied web assets app/src/main/assets/public + +# Sentry Config File +sentry.properties diff --git a/android/app/build.gradle b/android/app/build.gradle index fc5a8900b31..cf40c9f3b25 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,8 @@ + +plugins { + id 'io.sentry.android.gradle' version '4.1.1' +} + apply plugin: 'com.android.application' android { @@ -7,14 +12,15 @@ android { applicationId "com.logseq.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 77 - versionName "0.10.3" + versionCode 78 + versionName "0.10.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' } + manifestPlaceholders = [LOGSEQ_SENTRY_DSN: "$System.env.LOGSEQ_SENTRY_DSN"] } buildTypes { release { @@ -53,3 +59,13 @@ try { } catch(Exception e) { logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work") } + + +sentry { + org = "logseq" + projectName = "logseq" + + // this will upload your source code to Sentry to show it as part of the stack traces + // disable if you don't want to expose your sources + includeSourceContext = false +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index dcd579035bc..3b450542c88 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -60,5 +60,20 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> - + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/logseq/app/FsWatcher.java b/android/app/src/main/java/com/logseq/app/FsWatcher.java index 9d441c9e78d..0f896dd0946 100644 --- a/android/app/src/main/java/com/logseq/app/FsWatcher.java +++ b/android/app/src/main/java/com/logseq/app/FsWatcher.java @@ -101,7 +101,6 @@ public void onObserverEvent(int event, String path, SimpleFileMetadata metadata) if (relpath.startsWith("/")) { relpath = relpath.substring(1); } - relpath = Uri.decode(relpath); } else { Log.e("FsWatcher", "file path not under watch path"); return; diff --git a/deps/shui/deps.edn b/deps/shui/deps.edn index ccd9a316a28..f9549917cd8 100644 --- a/deps/shui/deps.edn +++ b/deps/shui/deps.edn @@ -1 +1,8 @@ -{:paths ["src"]} +{:paths ["src"] + :deps + {org.clojure/clojure {:mvn/version "1.11.1"} + org.clojure/clojurescript {:mvn/version "1.11.54"} + funcool/promesa {:mvn/version "4.0.2"} + rum/rum {:mvn/version "0.12.9"} + medley/medley {:mvn/version "1.4.0"} + cljs-bean/cljs-bean {:mvn/version "1.5.0"}}} diff --git a/deps/shui/src/logseq/shui/button/v2.cljs b/deps/shui/src/logseq/shui/button/v2.cljs deleted file mode 100644 index fbe36b43bfc..00000000000 --- a/deps/shui/src/logseq/shui/button/v2.cljs +++ /dev/null @@ -1,56 +0,0 @@ -(ns logseq.shui.button.v2 - (:require - [clojure.string :as str] - [rum.core :as rum] - [logseq.shui.icon.v2 :as icon] - [clojure.string :as string] - [goog.userAgent])) - -(rum/defcs root < rum/reactive - (rum/local nil ::hover-theme) - [state {:keys [theme hover-theme color text depth size icon interactive shortcut tiled tiles on-click muted disabled? class href button-props icon-props] - :or {theme :color depth 1 size :md interactive true muted false class ""}} context] - (let [*hover-theme (::hover-theme state) - color-string (or (some-> color name) (some-> context :state rum/react :ui/radix-color name) "custom") - theme (or @*hover-theme theme) - theme-class (str "ui__button-theme-" (if (keyword? theme) (name theme) "color")) - depth-class (when-not (= :text theme) (str "ui__button-depth-" depth)) - color-class (str "ui__button-color-" color-string) - muted-class (when muted "ui__button-muted") - size-class (str "ui__button-size-" (name size)) - tiled-class (when tiled "ui__button-tiled") - on-click (fn [e] - (when href (set! (.-href js/window.location) href)) - (when on-click (on-click e)))] - [:button.ui__button - (merge - button-props - (cond-> - {:class (str theme-class " " depth-class " " color-class " " size-class " " tiled-class " " muted-class " " class) - :disabled (boolean disabled?) - :on-mouse-over #(when hover-theme (reset! *hover-theme hover-theme)) - :on-mouse-out #(reset! *hover-theme nil)} - on-click - (assoc :on-click on-click))) - (if (and tiled (or text tiles)) - (for [[index tile] (map-indexed vector - (or tiles (and text (rest (string/split text #"")))))] - [:<> - (when (< 0 index) - [:div.ui__button__tile-separator]) - [:div.ui__button__tile tile]]) - text) - - (when icon - (icon/root icon icon-props)) - (when (not-empty shortcut) - (for [key shortcut] - [:div.ui__button-shortcut-key - (case key - "cmd" [:div (if goog.userAgent/MAC "⌘" "Ctrl")] - "shift" [:div "⇧"] - "return" [:div "⏎"] - "esc" [:div.tracking-tightest {:style {:transform "scaleX(0.8) scaleY(1.2) " - :font-size "0.5rem" - :font-weight "500"}} "ESC"] - (cond-> key (string? key) .toUpperCase))]))])) diff --git a/deps/shui/src/logseq/shui/context.cljs b/deps/shui/src/logseq/shui/context.cljs index e0579b31225..e90aa8d118c 100644 --- a/deps/shui/src/logseq/shui/context.cljs +++ b/deps/shui/src/logseq/shui/context.cljs @@ -10,12 +10,12 @@ (map #(inline* context %) col)))) (defn make-context [{:keys [block-config config inline int->local-time-2 blocks-container page-cp page] :as props}] - (merge props {;; Until components are converted over, they need to fallback to the old inline function + (merge props {;; 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 containing at least the following keys: - ;; These will be passed through in a whitelisted fashion so as to be able to track the dependencies + ;; 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 diff --git a/deps/shui/src/logseq/shui/core.cljs b/deps/shui/src/logseq/shui/core.cljs index a707b973d2c..bbc37ed58c2 100644 --- a/deps/shui/src/logseq/shui/core.cljs +++ b/deps/shui/src/logseq/shui/core.cljs @@ -1,8 +1,6 @@ (ns logseq.shui.core (:require - [logseq.shui.button.v2 :as shui.button.v2] [logseq.shui.context :as shui.context] - [logseq.shui.dialog.v1 :as shui.dialog.v1] [logseq.shui.icon.v2 :as shui.icon.v2] [logseq.shui.list-item.v1 :as shui.list-item.v1] [logseq.shui.table.v2 :as shui.table.v2] @@ -16,10 +14,6 @@ (def shortcut shui.shortcut.v1/root) (def shortcut-v1 shui.shortcut.v1/root) -;; button component -(def button shui.button.v2/root) -(def button-v2 shui.button.v2/root) - ;; icon (def icon shui.icon.v2/root) (def icon-v2 shui.icon.v2/root) @@ -28,9 +22,5 @@ (def list-item shui.list-item.v1/root) (def list-item-v1 shui.list-item.v1/root) -;; dialog -(def dialog shui.dialog.v1/root) -(def dialog-v1 shui.dialog.v1/root) - ;; context (def make-context shui.context/make-context) diff --git a/deps/shui/src/logseq/shui/demo.cljs b/deps/shui/src/logseq/shui/demo.cljs new file mode 100644 index 00000000000..3fa7ee99d0b --- /dev/null +++ b/deps/shui/src/logseq/shui/demo.cljs @@ -0,0 +1,460 @@ +(ns logseq.shui.demo + (:require [rum.core :as rum] + [logseq.shui.ui :as ui] + [logseq.shui.form.core :refer [yup yup-resolver] :as form-core] + [promesa.core :as p] + [logseq.shui.dialog.core :as dialog-core] + [cljs-bean.core :as bean])) + +(rum/defc section-item + [title children] + [:section.mb-4 + [:h2.text-xl.font-semibold.py-2.italic.opacity-50 title] + [:div.py-4 children]]) + +(rum/defc sample-dropdown-menu-content + [] + (let [icon #(ui/tabler-icon (name %1) {:class "scale-90 pr-1 opacity-80"})] + (ui/dropdown-menu-content + {:class "w-56" + :on-click (fn [^js e] (some-> (.-target e) (.-innerText) + (#(identity ["You select: " [:b.text-red-700 %1]])) (ui/toast! :info)))} + (ui/dropdown-menu-label "My Account") + (ui/dropdown-menu-separator) + (ui/dropdown-menu-group + ;; items + (ui/dropdown-menu-item (icon :user) "Profile" (ui/dropdown-menu-shortcut "⌘P")) + (ui/dropdown-menu-item (icon :brand-mastercard) [:span "Billing"] (ui/dropdown-menu-shortcut "⌘B")) + (ui/dropdown-menu-item (icon :adjustments-alt) [:span "Settings"] (ui/dropdown-menu-shortcut "⌘,")) + (ui/dropdown-menu-item (icon :keyboard) [:span "Keyboard shortcuts"])) + (ui/dropdown-menu-separator) + ;; group + (ui/dropdown-menu-group + ;; items + (ui/dropdown-menu-item (icon :users) "Team") + ;; sub menu + (ui/dropdown-menu-sub + (ui/dropdown-menu-sub-trigger + (icon :user-plus) [:span "Invite users"]) + (ui/dropdown-menu-sub-content + (ui/dropdown-menu-item (icon :mail) "Email") + (ui/dropdown-menu-item (icon :message) "Message") + (ui/dropdown-menu-item (icon :dots-circle-horizontal) "More..."))) + ;; menu item + (ui/dropdown-menu-item (icon :plus) "New Team" (ui/dropdown-menu-shortcut "⌘+T"))) + (ui/dropdown-menu-separator) + (ui/dropdown-menu-item (icon :brand-github) "GitHub") + (ui/dropdown-menu-item {:disabled true} (icon :cloud) "Cloud API") + (ui/dropdown-menu-separator) + (ui/dropdown-menu-item (icon :logout) "Logout" (ui/dropdown-menu-shortcut "⌘+Q")) + ))) + +(rum/defc sample-context-menu-content + [] + (let [icon #(ui/tabler-icon (name %1) {:class "scale-90 pr-1 opacity-80"})] + (ui/context-menu + ;; trigger + (ui/context-menu-trigger + [:div.border.px-6.py-12.border-dashed.rounded.text-center.select-none + {:key "ctx-menu-click"} + [:span.opacity-50 "Right click here"]]) + ;; content + (ui/context-menu-content + {:class "w-60 max-h-[80vh] overflow-auto"} + (ui/context-menu-item + (icon "arrow-left") + "Back" + (ui/context-menu-shortcut "⌘[")) + (ui/context-menu-item {:disabled true} + (icon "arrow-right") + "Forward" + (ui/context-menu-shortcut "⌘]")) + (ui/context-menu-item + (icon "refresh") + "Reload" + (ui/context-menu-shortcut "⌘R")) + ;; Sub menu + (ui/context-menu-sub + (ui/context-menu-sub-trigger {:inset true} "More tools") + (ui/context-menu-sub-content {:class "w-48"} + (ui/context-menu-item "Save page As..." + (ui/context-menu-shortcut "⇧⌘S")) + (ui/context-menu-item "Create Shortcut...") + (ui/context-menu-item "Name Window...") + (ui/context-menu-separator) + (ui/context-menu-item "Developer Tools"))) + ;; more + (ui/context-menu-separator) + (ui/context-menu-checkbox-item {:checked true} + "Show Bookmarks Bar" (ui/context-menu-shortcut "⌘⇧B")) + (ui/context-menu-checkbox-item "Show Full URLs") + (ui/context-menu-separator) + (ui/context-menu-radio-group {:value "pedro"} + (ui/context-menu-label {:inset true} "People") + (ui/context-menu-separator) + (ui/context-menu-radio-item {:value "pedro"} "Pedro Duarte") + (ui/context-menu-radio-item {:value "colm"} "Colm Tuite")))))) + +(rum/defc sample-form-basic + [] + [:div.border.p-6.rounded.bg-gray-01 + (let [form-ctx (form-core/use-form + {:defaultValues {:username "" + :agreement true + :notification "all" + :bio ""} + :yupSchema (-> (.object yup) + (.shape #js {:username (-> (.string yup) (.required))}) + (.required))}) + handle-submit (:handleSubmit form-ctx) + on-submit-valid (handle-submit + (fn [^js e] + (js/console.log "[form] submit: " e) + (js/alert (js/JSON.stringify e nil 2))))] + + (ui/form-provider form-ctx + [:form + {:on-submit on-submit-valid} + + ;; field item + (ui/form-field {:name "username"} + (fn [field error] + (ui/form-item + (ui/form-label "Username") + (ui/form-control + (ui/input (merge {:placeholder "Username"} field))) + (ui/form-description + (if error + [:b.text-red-800 (:message error)] + "This is your public display name."))))) + + (ui/form-field {:name "bio"} + (fn [field error] + (ui/form-item + {:class "pt-4"} + (ui/form-control + (ui/textarea (merge {:placeholder "Bio text..."} field)))))) + + ;; radio + (ui/form-field {:name "notification"} + ;; item render + (fn [field] + (ui/form-item + {:class "space-y-3 my-4"} + (ui/form-label "Notify me about...") + (ui/form-control + (ui/radio-group + {:value (:value field) + :on-value-change (:onChange field) + :class "flex flex-col space-y-3"} + (ui/form-item + {:class "flex flex-row space-x-3 items-center space-y-0"} + (ui/form-control + (ui/radio-group-item {:value "all"})) + (ui/form-label "All")) + + (ui/form-item + {:class "flex flex-row space-x-3 items-center space-y-0"} + (ui/form-control + (ui/radio-group-item {:value "direct"})) + (ui/form-label "Direct messages and mentions"))))))) + + [:hr] + + ;; checkbox + (ui/form-field {:name "agreement"} + (fn [field] + (ui/form-item + {:class "flex justify-start items-center space-x-3 space-y-0 my-3 pr-3"} + (ui/form-control + (ui/checkbox {:checked (:value field) + :on-checked-change (:onChange field)})) + (ui/form-label {:class "font-normal cursor-pointer"} "Agreement terms")))) + + ;; actions + [:div.relative.px-2 + (ui/button {:type "submit" :class "!absolute right-0 top-[-40px]"} "Submit")]]))]) + +(rum/defc sample-date-picker + [] + (let [[open? set-open!] (rum/use-state false) + [date set-date!] (rum/use-state (js/Date.))] + (ui/popover + {:open open? + :on-open-change (fn [o] (set-open! o))} + ;; trigger + (ui/popover-trigger + {:as-child true + :class "w-2/3"} + (ui/input + {:type :text + :placeholder "pick a date" + :default-value (.toDateString date)})) + ;; content + (ui/popover-content + {:on-open-auto-focus #(.preventDefault %) + :side-offset 8 + :class "p-0"} + (ui/calendar + {:selected date + :on-day-click + (fn [^js d] + (set-date! d) + (set-open! false))}))))) + +(rum/defc sample-dialog-basic + [] + (let [[open? set-open!] (rum/use-state false)] + (ui/dialog + {:open open? + :on-open-change #(set-open! %)} + (ui/dialog-trigger + {:as-child true} + (ui/button {:variant :outline} + (ui/tabler-icon "notification") "Open as modal locally")) + (ui/dialog-content + (ui/dialog-header + (ui/dialog-title "Header") + (ui/dialog-description + "Description")) + [:div.max-h-96.overflow-y-auto + {:class "-mx-6"} + [:section.px-6 + (repeat 8 [:p "Your custom content"])]] + (ui/dialog-footer + (ui/button + {:on-click #(set-open! false) + :size :md} "🍄 * Footer")))))) + +(rum/defc page [] + [:div.sm:p-10 + [:h1.text-3xl.font-bold "Logseq UI"] + [:hr] + + ;; Button + (section-item "Button" + [:div.flex.flex-row.flex-wrap.gap-2 + (let [[loading? set-loading!] (rum/use-state false)] + (ui/button + {:size :sm + :on-click (fn [] + (set-loading! true) + (js/setTimeout #(set-loading! false) 5000)) + :disabled loading?} + (when loading? + (ui/tabler-icon "loader2" {:class "animate-spin"})) + "Logseq Classic Button" + (ui/tabler-icon "arrow-right"))) + + (ui/button {:variant :outline :size :sm} "Outline") + (ui/button {:variant :secondary :size :sm} "Secondary") + (ui/button {:disabled true :size :sm} "Disabled") + (ui/button {:variant :destructive :size :sm} "Destructive") + (ui/button {:class "primary-green" :size :sm} "Custom (.primary-green)") + (ui/button {:variant :ghost :size :sm} "Ghost") + (ui/button {:variant :link :size :sm} "Link") + (ui/button + {:variant :icon + :size :sm} + [:a.flex.items-center.text-blue-rx-10.hover:text-blue-rx-10-alpha + {:href "https://x.com/logseq" :target "_blank"} + (ui/tabler-icon "brand-twitter" {:size 15})] + )]) + + ;; Toast + (section-item "Toast" + [:div.flex.flex-row.flex-wrap.gap-2 + (ui/button + {:size :md + :variant :outline + :on-click #(ui/toast! + "Check for updates ..." + (nth [:success :error :default :info :warning] (rand-int 3)) + {:title (if (odd? (js/Date.now)) "History of China" "") + :duration 3000})} + "Open random toast" + (ui/tabler-icon "arrow-right")) + + (ui/button + {:variant :secondary + :size :md + :on-click (fn [] + (ui/toast! + (fn [{:keys [id dismiss! update!]}] + [:b.text-red-700 + [:div.flex.items-center.gap-2 + (ui/tabler-icon "info-circle") + (str "#(" id ") ") + (.toLocaleString (js/Date.))] + [:div.flex.flex-row.gap-2 + (ui/button + {:on-click #(dismiss! id) :size :sm} + "x close") + + (ui/button + {:on-click #(update! {:title (js/Date.now) + :action [:b (ui/button {:on-click (fn [] (ui/toast-dismiss!))} "clear all")]}) + :size :sm} + "x update")]]) + :default + {:duration 3000 :onDismiss #(js/console.log "===>> dismiss?:" %1)}))} + (ui/tabler-icon "apps") + "Toast callback handle") + + (ui/button + {:on-click #(ui/toast! "A message from SoundCloud..." + {:class "text-orange-rx-10" + :icon [:b.pl-1 (ui/tabler-icon "brand-soundcloud" {:size 20})] + :duration 3000}) + :class "primary-orange" + :size :md} + "Custom icon")]) + + ;; Tips + (section-item "Tips" + [:div.flex.flex-row.flex-wrap.gap-2 + (ui/tooltip-provider + (ui/tooltip + (ui/tooltip-trigger + (ui/button + {:variant :outline + :on-click #(dialog-core/open! [:h1.text-9xl.text-center.scale-110 "🍄"])} + "Tip for hint?")) + (ui/tooltip-content + {:class "w-42 px-8 py-4 text-xl border-green-rx-08 bg-green-rx-07-alpha"} + "🍄")))]) + + ;; Badge + (section-item "Badge" + [:div.flex.flex-row.flex-wrap.gap-2 + (ui/badge "Default") + (ui/badge {:variant :outline} "Outline") + (ui/badge {:variant :secondary} "Secondary") + (ui/badge {:variant :destructive} "Destructive") + (ui/badge {:class "primary-yellow"} "Custom (.primary-yellow)")]) + + [:div.grid.sm:grid-cols-3.sm:gap-8 + ;; Dropdown + (section-item "Dropdown" + (ui/dropdown-menu + (ui/dropdown-menu-trigger + {:as-child true} + (ui/button {:variant :outline} + (ui/tabler-icon "list") "Open dropdown menu")) + (sample-dropdown-menu-content))) + + ;; Context menu + [:div.col-span-2 + (section-item "Context Menu" + (sample-context-menu-content))]] + + ;; Dialog + (section-item "Dialog" + [:div.flex.flex-row.flex-wrap.gap-2 + (sample-dialog-basic) + (ui/button + {:on-click #(dialog-core/open! "a modal dialog from `open!`" {:title "Title"})} + "Imperative API: open!") + + (ui/button + {:class "primary-yellow" + :on-click (fn [] + (-> (dialog-core/alert! + "a alert dialog from `alert!`" + {:title [:div.flex.flex-row.space-x-2.items-center + (ui/tabler-icon "alert-triangle" {:size 18}) + [:span "Alert"]]}) + (p/then #(js/console.log "=> alert (promise): " %))))} + "Imperative API: alert!") + + (ui/button + {:class "primary-green" + :on-click (fn [] + (-> (dialog-core/confirm! + "a alert dialog from `confirm!`" + {:title [:div.flex.flex-row.space-x-2.items-center + (ui/tabler-icon "alert-triangle" {:size 18}) + [:span "Confirm"]]}) + (p/then #(js/console.log "=> confirm (promise): " %)) + (p/catch #(js/console.log "=> confirm (promise): " %))))} + "Imperative API: confirm!")]) + + ;; Alert + (section-item "Alert" + [:<> + (ui/alert + {:class "text-orange-rx-09 border-orange-rx-07-alpha mb-4"} + (ui/tabler-icon "brand-soundcloud") + (ui/alert-title "Title is SoundCloud") + (ui/alert-description + "content: radix colors for Logseq")) + (ui/alert + (ui/tabler-icon "brand-github") + (ui/alert-title "GitHub") + (ui/alert-description + "content: radix colors for Logseq"))]) + + ;; Slider + [:div.grid.sm:grid-cols-8.gap-4 + [:div.col-span-4.mr-6 + (section-item "Slider" (ui/slider))] + [:div.col-span-1 + (section-item "Switch" + (ui/switch {:size :sm :class "relative top-[-8px]"}))] + [:div.col-span-3.pl-4.pr-2 + (section-item "Select" + (ui/select + {:on-value-change (fn [v] (ui/toast! v :info))} + ;; trigger + (ui/select-trigger + (ui/select-value {:placeholder "Select a fruit"})) + ;; content + (ui/select-content + (ui/select-group + (ui/select-label "Fruits") + (ui/select-item {:value "apple"} "Apple") + (ui/select-item {:value "pear"} "Pear") + (ui/select-item {:value "grapes"} "Grapes") + + ))))]] + + ;; Form + (section-item "Form" + [:<> + (sample-form-basic)]) + + ;; Card + [:div.grid.sm:grid-cols-2.sm:gap-8 + (section-item "Card" + (ui/card + (ui/card-header + (ui/card-title "Title") + (ui/card-description "Description")) + (ui/card-content "This is content") + (ui/card-footer "Footer"))) + + (section-item "Skeleton" + (ui/card + (ui/card-header + (ui/card-title + (ui/skeleton {:class "h-4 w-1/2"})) + (ui/card-description + (ui/skeleton {:class "h-2 w-full"}))) + (ui/card-content + (ui/skeleton {:class "h-3 mb-1"}) + (ui/skeleton {:class "h-3 mb-1"}) + (ui/skeleton {:class "h-3 w-2/3"})) + + (ui/card-footer + (ui/skeleton {:class "h-4 w-full mb-2"}))))] + + ;; Calendar + [:div.grid.sm:grid-cols-2.sm:gap-8 + (section-item "Calendar" + (ui/card + {:class "inline-flex"} + (ui/calendar {:on-day-click #(ui/toast! (.toString %) :success)}))) + (section-item "Date Picker" + (sample-date-picker))] + + [:hr.mb-80]]) diff --git a/deps/shui/src/logseq/shui/dialog/core.cljs b/deps/shui/src/logseq/shui/dialog/core.cljs new file mode 100644 index 00000000000..ceb042b6455 --- /dev/null +++ b/deps/shui/src/logseq/shui/dialog/core.cljs @@ -0,0 +1,180 @@ +(ns logseq.shui.dialog.core + (:require [rum.core :as rum] + [daiquiri.interpreter :refer [interpret]] + [medley.core :as medley] + [logseq.shui.util :as util] + [promesa.core :as p] + [clojure.string :as string])) + +;; provider +(def dialog (util/lsui-wrap "Dialog")) +(def dialog-portal (util/lsui-wrap "DialogPortal")) +(def alert-dialog (util/lsui-wrap "AlertDialog")) +(def alert-dialog-portal (util/lsui-wrap "AlertDialogPortal")) + +;; ui +(def dialog-overlay (util/lsui-wrap "DialogOverlay")) +(def dialog-close (util/lsui-wrap "DialogClose")) +(def dialog-trigger (util/lsui-wrap "DialogTrigger")) +(def dialog-content (util/lsui-wrap "DialogContent")) +(def dialog-header (util/lsui-wrap "DialogHeader")) +(def dialog-footer (util/lsui-wrap "DialogFooter")) +(def dialog-title (util/lsui-wrap "DialogTitle")) +(def dialog-description (util/lsui-wrap "DialogDescription")) +(def alert-dialog-overlay (util/lsui-wrap "AlertDialogOverlay")) +(def alert-dialog-trigger (util/lsui-wrap "AlertDialogTrigger")) +(def alert-dialog-content (util/lsui-wrap "AlertDialogContent")) +(def alert-dialog-header (util/lsui-wrap "AlertDialogHeader")) +(def alert-dialog-title (util/lsui-wrap "AlertDialogTitle")) +(def alert-dialog-description (util/lsui-wrap "AlertDialogDescription")) +(def alert-dialog-footer (util/lsui-wrap "AlertDialogFooter")) +(def alert-dialog-action (util/lsui-wrap "AlertDialogAction")) +(def alert-dialog-cancel (util/lsui-wrap "AlertDialogCancel")) + +(defn interpret-vals + [config ks & args] + (reduce (fn [config k] + (let [v (get config k) + v (if (fn? v) (apply v args) v)] + (if (vector? v) (assoc config k (interpret v)) config))) + config ks)) + +;; {:id :title :description :content :footer :open? ...} +(def ^:private *modals (atom [])) +(def ^:private *id (atom 0)) +(def ^:private gen-id #(reset! *id (inc @*id))) + +(defn get-modal + [id] + (when id + (some->> (medley/indexed @*modals) + (filter #(= id (:id (second %)))) (first)))) + +(defn update-modal! + [id ks val] + (when-let [[index config] (get-modal id)] + (let [ks (if (coll? ks) ks [ks]) + config (if (nil? val) + (medley/dissoc-in config ks) + (assoc-in config ks val))] + (swap! *modals assoc index config)))) + +(defn upsert-modal! + [config] + (when-let [_id (:id config)] + (swap! *modals conj config))) + +(defn detach-modal! + [id] + (when-let [[index] (get-modal id)] + (swap! *modals #(->> % (medley/remove-nth index) (vec))))) + +(rum/defc modal-inner + [config] + (let [{:keys [id title description content footer on-open-change open?]} config + props (dissoc config :id :title :description :content :footer :on-open-change :open?)] + + (rum/use-effect! + (fn [] + (when (false? open?) + (js/setTimeout #(detach-modal! id) 128))) + [open?]) + + (dialog + {:key (str "modal-" id) + :open open? + :on-open-change (fn [v] + (let [set-open! #(update-modal! id :open? %)] + (if (fn? on-open-change) + (on-open-change {:value v :set-open! set-open!}) + (set-open! v))))} + (dialog-content props + (dialog-header + (when title (dialog-title title)) + (when description (dialog-description description))) + (when content + [:div.ui__dialog-main-content content]) + (when footer + (dialog-footer footer)))))) + +(rum/defc alert-inner + [config] + (let [{:keys [id title description content footer deferred open?]} config + props (dissoc config :id :title :description :content :footer :deferred :open? :alert?)] + + (rum/use-effect! + (fn [] + (when (false? open?) + (js/setTimeout #(detach-modal! id) 128))) + [open?]) + + (alert-dialog + {:key (str "alert-" id) + :open open? + :on-open-change #(update-modal! id :open? %)} + (alert-dialog-content props + (alert-dialog-header + (when title (alert-dialog-title title)) + (when description (alert-dialog-description description))) + (when content + [:div.ui__alert-dialog-main-content content]) + (alert-dialog-footer + (if footer + footer + [:<> (alert-dialog-action {:key "ok" :on-click #(p/resolve! deferred true)} "OK")])))))) + +(rum/defc confirm-inner + [config] + (let [{:keys [deferred]} config] + (alert-inner + (assoc config :footer + [:<> + (alert-dialog-cancel {:key "cancel" :on-click #(p/reject! deferred false)} "Cancel") + (alert-dialog-action {:key "ok" :on-click #(p/resolve! deferred true)} "OK")])))) + +(rum/defc install-modals + < rum/static + [] + (let [[modals _set-modals!] (util/use-atom *modals)] + (for [config modals + :when (map? config)] + (let [id (:id config) + alert? (:alert? config) + config (interpret-vals config + [:title :description :content :footer] + {:id id})] + (case alert? + :default + (alert-inner config) + :confirm + (confirm-inner config) + ;; modal + (modal-inner config)))))) + +;; apis +(defn open! + [content-or-config & config'] + (let [config (if (map? content-or-config) + content-or-config + {:content content-or-config}) + config (merge config (first config'))] + (upsert-modal! + (merge {:id (gen-id) :open? true} config)))) + +(defn alert! + [content-or-config & config'] + (let [deferred (p/deferred)] + (open! content-or-config + (merge {:alert? :default :deferred deferred} (first config'))) + (p/promise deferred))) + +(defn confirm! + [content-or-config & config'] + (alert! content-or-config (assoc (first config') :alert? :confirm))) + +(defn close! [id] + (update-modal! id :open? false)) + +(defn close-all! [] + (doseq [{:keys [id]} @*modals] + (close! id))) \ No newline at end of file diff --git a/deps/shui/src/logseq/shui/dialog/v1.cljs b/deps/shui/src/logseq/shui/dialog/v1.cljs deleted file mode 100644 index 67700bebecb..00000000000 --- a/deps/shui/src/logseq/shui/dialog/v1.cljs +++ /dev/null @@ -1,68 +0,0 @@ -(ns logseq.shui.dialog.v1 - (:require - [rum.core :as rum] - [clojure.string :as string] - [logseq.shui.icon.v2 :as icon] - [logseq.shui.button.v2 :as button])) - -(defn open-dialog! [state position] - (js/console.log "open-dialog!") - (when-let [el (some-> state ::dialog-ref deref)] - (if (= position :modal) - (.showModal ^js el) - (.show ^js el)) - (reset! (::open state) true))) - -(defn close-dialog! [state] - (js/console.log "close-dialog!") - (when-let [el (some-> state ::dialog-ref deref)] - (.close ^js el) - (reset! (::open state) false))) - -(defn toggle-dialog! [state position] - (js/console.log "toggle-dialog!") - (if @(::open state) - (close-dialog! state) - (open-dialog! state position))) - -(rum/defc dialog < rum/reactive - [state props context] - [:dialog {:ref #(when (and % (::dialog-ref state) (not= % (::dialog-ref state))) - (js/console.log "set dialog ref" %) - (reset! (::dialog-ref state) %)) - :class "text-xs bg-gray-03 right-full top-full text-white absolute left-0 w-64 p-0 rounded-lg shadow-lg overflow-hidden -border border-gray-06 py-2" - :style {:transform "translate3d(calc(-100% + 32px), 4px, 0) "} - :open @(::open state)} - (for [[index group] (map-indexed vector (:groups props))] - [:div {:key index} - group])]) - ; (for [[index list-item] (map-indexed vector group)] - ; [:div {:key index} - ; list])])]) - ; [:div.bg-gray-05 - ; [:h1 "This is a dialog"]]]) - ; [:div.absolute.top-full.right-0.bg-gray-05 - ; [:h1 "This is a dialog"]]]) - - -(rum/defcs root < rum/reactive - (rum/local true ::open) - (rum/local nil ::dialog-ref) - [state - {:keys [open position trigger] :as props - :or {position :top-right}} - {:keys [] :as context}] - ; (rum/use-effect! - ; (fn [] - ; (when (and @(::dialog-ref state) - ; (not= @(::open state) open)) - ; (if open - ; (open-dialog! state position) - ; (close-dialog! state)))) - ; [@(::dialog-ref state) open]) - (if trigger - (trigger {:open-dialog! #(open-dialog! state position) - :close-dialog! #(close-dialog! state) - :toggle-dialog! #(toggle-dialog! state position) - :dialog (partial dialog state props context)}) - (dialog state props context))) diff --git a/deps/shui/src/logseq/shui/form/core.cljs b/deps/shui/src/logseq/shui/form/core.cljs new file mode 100644 index 00000000000..615d67599ae --- /dev/null +++ b/deps/shui/src/logseq/shui/form/core.cljs @@ -0,0 +1,53 @@ +(ns logseq.shui.form.core + (:require [rum.core :as rum] + [daiquiri.interpreter :refer [interpret]] + [logseq.shui.util :as util] + [cljs-bean.core :as bean])) + + +;; State +(def form-provider (util/lsui-wrap "Form" {:static? false})) +(def form-field' (util/lsui-wrap "FormField" {:static? false})) + +(rum/defc form-field + [render' & args] + (let [[props render'] + (if (map? render') + [render' (first args)] + [(first args) render']) + _ (assert (contains? props :name) ":name is required for ") + render (fn [^js ctx] + ;; TODO: convert field-state? + (render' + (bean/bean (.-field ctx)) + (some-> (.-fieldState ctx) (.-error) (bean/bean)) + (bean/bean (.-fieldState ctx)) + ctx))] + (form-field' (assoc props :render render)))) + +(def form-control (util/lsui-wrap "FormControl" {:static? false})) + +(def ^js yup (util/lsui-get "yup")) +(def yup-resolver (util/lsui-get "yupResolver")) + +;; Hooks +;; https://react-hook-form.com/docs/useform#resolver +(def use-form' (util/lsui-get "useForm")) +(def use-form-context (util/lsui-get "useFormContext")) + +(defn use-form + ([] (use-form {})) + ([opts] + (let [yup-schema (:yupSchema opts) + ^js methods (use-form' (bean/->js + (cond-> opts + (not (nil? yup-schema)) + (assoc :resolver (yup-resolver yup-schema)))))] + ;; NOTE: just shallow convert return object! + (bean/bean methods)))) + +;; UI +(def form-item (util/lsui-wrap "FormItem")) +(def form-label (util/lsui-wrap "FormLabel")) +(def form-description (util/lsui-wrap "FormDescription")) +(def form-message (util/lsui-wrap "FormMessage")) \ No newline at end of file diff --git a/deps/shui/src/logseq/shui/icon/v2.cljs b/deps/shui/src/logseq/shui/icon/v2.cljs index 08d644276be..05e96b5a14e 100644 --- a/deps/shui/src/logseq/shui/icon/v2.cljs +++ b/deps/shui/src/logseq/shui/icon/v2.cljs @@ -1,74 +1,18 @@ (ns logseq.shui.icon.v2 - (:require - [camel-snake-kebab.core :as csk] - [cljs-bean.core :as bean] - [clojure.set :as set] - [clojure.string :as string] - [clojure.walk :as w] - [daiquiri.interpreter :as interpreter] - [goog.object :as gobj] - [goog.string :as gstring] - [rum.core :as rum])) - -;; this is taken from frontend.rum, and should be properly abstracted -(defn kebab-case->camel-case - "Converts from kebab case to camel case, eg: on-click to onClick" - [input] - (string/replace input #"-([a-z])" (fn [[_ c]] (string/upper-case c)))) - -;; this is taken from frontend.rum, and should be properly abstracted -(defn map-keys->camel-case - "Stringifys all the keys of a cljs hashmap and converts them - from kebab case to camel case. If :html-props option is specified, - then rename the html properties values to their dom equivalent - before conversion" - [data & {:keys [html-props]}] - (let [convert-to-camel (fn [[key value]] - [(kebab-case->camel-case (name key)) value])] - (w/postwalk (fn [x] - (if (map? x) - (let [new-map (if html-props - (set/rename-keys x {:class :className :for :htmlFor}) - x)] - (into {} (map convert-to-camel new-map))) - x)) - data))) - -;; this is taken from frontend.rum, and should be properly abstracted -(defn adapt-class - ([react-class] - (adapt-class react-class false)) - ([react-class skip-opts-transform?] - (fn [& args] - (let [[opts children] (if (map? (first args)) - [(first args) (rest args)] - [{} args]) - type# (first children) - ;; we have to make sure to check if the children is sequential - ;; as a list can be returned, eg: from a (for) - new-children (if (sequential? type#) - (let [result (interpreter/interpret children)] - (if (sequential? result) - result - [result])) - children) - ;; convert any options key value to a react element, if - ;; a valid html element tag is used, using sablono - vector->react-elems (fn [[key val]] - (if (sequential? val) - [key (interpreter/interpret val)] - [key val])) - new-options (into {} - (if skip-opts-transform? - opts - (map vector->react-elems opts)))] - (apply js/React.createElement react-class - ;; sablono html-to-dom-attrs does not work for nested hashmaps - (bean/->js (map-keys->camel-case new-options :html-props true)) - new-children))))) + (:require + [camel-snake-kebab.core :as csk] + [cljs-bean.core :as bean] + [clojure.set :as set] + [clojure.string :as string] + [clojure.walk :as w] + [daiquiri.interpreter :as interpreter] + [goog.object :as gobj] + [goog.string :as gstring] + [logseq.shui.util :as shui-utils] + [rum.core :as rum])) (def get-adapt-icon-class - (memoize (fn [klass] (adapt-class klass)))) + (memoize (fn [klass] (shui-utils/react->rum klass true)))) (rum/defc root ([name] (root name nil)) @@ -79,14 +23,14 @@ [:span.ui__icon (merge {:class (gstring/format (str "%s-" name - (when (:class opts) - (str " " (string/trim (:class opts))))) + (when (:class opts) + (str " " (string/trim (:class opts))))) (if extension? "tie tie" "ti ti"))} - (dissoc opts :class :extension? :font?))] + (dissoc opts :class :extension? :font?))] ;; tabler svg react (when-let [klass (gobj/get js/tablerIcons (str "Icon" (csk/->PascalCase name)))] - (let [f (get-adapt-icon-class klass)] + (let [f (shui-utils/component-wrap js/tablerIcons (str "Icon" (csk/->PascalCase name)))] [:span.ui__icon.ti {:class (str "ls-icon-" name " " class)} - (f (merge {:size 18} (map-keys->camel-case (dissoc opts :class))))]))))))) + (f (merge {:size 18} (shui-utils/map-keys->camel-case (dissoc opts :class))))]))))))) diff --git a/deps/shui/src/logseq/shui/list_item/v1.cljs b/deps/shui/src/logseq/shui/list_item/v1.cljs index c8c13241959..fc8b506c2c6 100644 --- a/deps/shui/src/logseq/shui/list_item/v1.cljs +++ b/deps/shui/src/logseq/shui/list_item/v1.cljs @@ -77,7 +77,7 @@ (rum/defc root [{:keys [icon icon-theme query text info shortcut value-label value title highlighted on-highlight on-highlight-dep header on-click - hoverable compact rounded on-mouse-enter component-opts] :as _props + hoverable compact rounded on-mouse-enter component-opts source-page] :as _props :or {hoverable true rounded true}} {:keys [app-config] :as context}] (let [ref (rum/create-ref) @@ -124,7 +124,13 @@ [:div.flex.flex-1.flex-col (when title [:div.text-sm.pb-2.font-bold.text-gray-11 (highlight-query title)]) - [:div {:class "text-sm font-medium text-gray-12"} (highlight-query text) + [:div {:class "text-sm font-medium text-gray-12"} + (if (and (= icon "page") (not= text source-page)) ;; alias + [:div.flex.flex-row.items-center.gap-2 + (highlight-query text) + [:div.opacity-50.font-normal "alias of"] + source-page] + (highlight-query text)) (when info [:span.text-xs.text-gray-11 " — " (highlight-query info)])]] (when (or value-label value) @@ -138,4 +144,4 @@ (when shortcut [:div {:class "flex gap-1" :style {:opacity (if (or highlighted hover?) 1 0.5)}} - (shortcut/root shortcut context)])]])) + (shortcut/root shortcut)])]])) diff --git a/deps/shui/src/logseq/shui/rum.cljs b/deps/shui/src/logseq/shui/rum.cljs new file mode 100644 index 00000000000..5d2a8df31b7 --- /dev/null +++ b/deps/shui/src/logseq/shui/rum.cljs @@ -0,0 +1,79 @@ +(ns logseq.shui.rum + (:require [clojure.string :as str] + [daiquiri.normalize :as normalize] + [daiquiri.util :as util] + [cljsjs.react] + [goog.object :as gobj])) + +(defn ^js/React.Element create-element + "Create a React element. Returns a JavaScript object when running + under ClojureScript, and a om.dom.Element record in Clojure." + [type attrs children] + (.apply (.-createElement js/React) nil (.concat #js [type attrs] children))) + +(defn component-attributes [attrs] + (let [x (util/camel-case-keys* attrs)] + (let [m (js-obj)] + (doseq [[k v] x] + (gobj/set m (name k) v)) + m))) + +(defn element-attributes [attrs] + (when-let [^js js-attrs (clj->js (util/html-to-dom-attrs attrs))] + (let [class (.-className js-attrs) + class (if (array? class) (str/join " " class) class)] + (when (.-onChange js-attrs) + ;; Wrapping on-change handler to work around async rendering queue + ;; that causes jumping caret and lost characters in input fields + (set! (.-onChange js-attrs) (js/rum.core.mark_sync_update (.-onChange js-attrs)))) + (if (str/blank? class) + (js-delete js-attrs "className") + (set! (.-className js-attrs) class)) + js-attrs))) + +(declare interpret) + +(defn- ^array interpret-seq + "Eagerly interpret the seq `x` as HTML elements." + [x] + (reduce + (fn [ret x] + (conj ret (interpret x))) + [] x)) + +(defn element + "Render an element vector as a HTML element." + [element] + (let [[type attrs content] (normalize/element element)] + (create-element type + (element-attributes attrs) + (interpret-seq content)))) + +(defn fragment [[_ attrs & children]] + (let [[attrs children] (if (map? attrs) + [(component-attributes attrs) (interpret-seq children)] + [nil (interpret-seq (into [attrs] children))])] + (create-element js/React.Fragment attrs children))) + +(defn interop [[_ component attrs & children]] + (let [[attrs children] (if (map? attrs) + [(component-attributes attrs) (interpret-seq children)] + [nil (interpret-seq (into [attrs] children))])] + (create-element component attrs children))) + +(defn- interpret-vec + "Interpret the vector `x` as an HTML element or a the children of an + element." + [x] + (cond + (util/fragment? x) (fragment x) + (keyword-identical? :> (nth x 0 nil)) (interop x) + (util/element? x) (element x) + :else (interpret-seq x))) + +(defn interpret [v] + (cond + (vector? v) (interpret-vec v) + (seq? v) (interpret-seq v) + :else v)) + diff --git a/deps/shui/src/logseq/shui/select/core.cljs b/deps/shui/src/logseq/shui/select/core.cljs new file mode 100644 index 00000000000..2eb50a5b084 --- /dev/null +++ b/deps/shui/src/logseq/shui/select/core.cljs @@ -0,0 +1,16 @@ +(ns logseq.shui.select.core + (:require [rum.core :as rum] + [daiquiri.interpreter :refer [interpret]] + [logseq.shui.util :as util] + [cljs-bean.core :as bean])) + +(def select (util/lsui-wrap "Select")) +(def select-group (util/lsui-wrap "SelectGroup")) +(def select-value (util/lsui-wrap "SelectValue")) +(def select-trigger (util/lsui-wrap "SelectTrigger")) +(def select-content (util/lsui-wrap "SelectContent")) +(def select-label (util/lsui-wrap "SelectLabel")) +(def select-item (util/lsui-wrap "SelectItem")) +(def select-separator (util/lsui-wrap "SelectSeparator")) +(def select-scroll-up-button (util/lsui-wrap "SelectScrollUpButton")) +(def select-scroll-down-button (util/lsui-wrap "SelectScrollDownButton")) diff --git a/deps/shui/src/logseq/shui/shortcut/v1.cljs b/deps/shui/src/logseq/shui/shortcut/v1.cljs index b83e394748c..5e209066cb3 100644 --- a/deps/shui/src/logseq/shui/shortcut/v1.cljs +++ b/deps/shui/src/logseq/shui/shortcut/v1.cljs @@ -1,6 +1,6 @@ (ns logseq.shui.shortcut.v1 (:require [clojure.string :as string] - [logseq.shui.button.v2 :as button] + [logseq.shui.ui :as ui] [rum.core :as rum] [goog.userAgent])) @@ -62,19 +62,22 @@ %))))))) (rum/defc part - [context theme ks size] - (button/root {:theme theme + [ks size] + (let [tiles (map print-shortcut-key ks)] + (ui/button {:variant :default + :class "bg-gray-03 text-gray-12 px-1.5 py-0 leading-4 h-5 hover:bg-gray-04 active:bg-gray-03 hover:text-gray-11" :interactive false - :tiled true - :tiles (map print-shortcut-key ks) - :size size - :mused true} - context)) + :size size} + (for [[index tile] (map-indexed vector tiles)] + [:<> + (when (< 0 index) + [:div.ui__button__tile-separator]) + [:div.ui__button__tile tile]])))) (rum/defc root - [shortcut context & {:keys [size theme] - :or {size :sm - theme :gray}}] + [shortcut & {:keys [size theme] + :or {size :xs + theme :gray}}] (when (seq shortcut) (let [shortcuts (if (coll? shortcut) [shortcut] @@ -85,5 +88,5 @@ [:div.text-gray-11.text-sm "|"]) (if (coll? (first binding)) ; + included (for [ks binding] - (part context theme ks size)) - (part context theme binding size))])))) + (part ks size)) + (part binding size))])))) diff --git a/deps/shui/src/logseq/shui/stories/badge_story.cljs b/deps/shui/src/logseq/shui/stories/badge_story.cljs new file mode 100644 index 00000000000..3ea849a581b --- /dev/null +++ b/deps/shui/src/logseq/shui/stories/badge_story.cljs @@ -0,0 +1,16 @@ +(ns logseq.shui.stories.badge-story + (:require [logseq.shui.ui :as ui] + [cljs-bean.core :as bean] + [rum.core :as rum]) + (:require-macros [logseq.shui.storybook :refer [defmeta defstory]])) + +(defmeta + :Shui/Badge + {:component ui/badge + :argTypes {:variant {:control :select + :options [:default :destructive :outline :secondary]} + :class {:control {:type :text}}} + :args {:children "a badge" + :class ""}}) + +(defstory Default {}) \ No newline at end of file diff --git a/deps/shui/src/logseq/shui/stories/button_story.cljs b/deps/shui/src/logseq/shui/stories/button_story.cljs new file mode 100644 index 00000000000..e86f115fa92 --- /dev/null +++ b/deps/shui/src/logseq/shui/stories/button_story.cljs @@ -0,0 +1,49 @@ +(ns logseq.shui.stories.button-story + (:require [logseq.shui.ui :as ui] + [cljs-bean.core :as bean] + [rum.core :as rum]) + (:require-macros [logseq.shui.storybook :refer [defmeta defstory]])) + +(defmeta + :Shui/Button + {:component ui/button + :tags ["autodocs"] + :argTypes {:size {:control :select + :options [:default :md :sm :xs :lg :icon]} + :variant {:control :select + :options [:default :solid :destructive :outline :secondary :ghost :link]} + :disabled {:control :boolean} + :children {:description "`string` | `ReactElement`" + :control {:hideNoControlsWarning true}}} + :args {:children "Button" + :disabled false + :variant :default}}) + +(defstory Primary + {:args + {:variant :default + :size :sm + :children "Primary button"}}) + +(defstory Secondary + {:args + {:variant :secondary + :children + (fn [] + [:<> + (ui/tabler-icon "brand-soundcloud") + "Get Logseq Desktop" + (ui/tabler-icon "arrow-right")])}}) + +(defstory LoadingButton + {:args + {:children + (fn [] + [:<> + (ui/tabler-icon "loader" {:class "animate-spin"}) + "Loading Button with custom icon"])}}) + + + + + diff --git a/deps/shui/src/logseq/shui/stories/toaster_story.cljs b/deps/shui/src/logseq/shui/stories/toaster_story.cljs new file mode 100644 index 00000000000..fd779051cc5 --- /dev/null +++ b/deps/shui/src/logseq/shui/stories/toaster_story.cljs @@ -0,0 +1,78 @@ +(ns logseq.shui.stories.toaster-story + (:require [logseq.shui.ui :as ui] + [logseq.shui.toaster.core :as toaster] + [rum.core :as rum]) + (:require-macros [logseq.shui.storybook :refer [defmeta defstory]])) + +(defmeta + :Shui/Toaster + {:component #() + :tags ["autodocs"] + :argTypes {:title {:control :text + :description "`string` | `(ctx) => void` | `ReactElement`"} + :description {:control :text + :description "`string` | `(ctx) => void` | `ReactElement`"} + :duration {:control :number + :description "milliseconds or 0 for not auto close!" + :table {:defaultValue {:summary 5000}}} + :variant {:control :select + :description "-" + :options [:default :destructive :info :success :warning :error]} + :onDismiss {:type :function + :description "hook on the toast item dismissed `func`" + :control {:hideNoControlsWarning true}} + :#Shadcn {:description "https://ui.shadcn.com/docs/components/toast" + :control {:hideNoControlsWarning true} + :table {:category :more + :type {:detail nil}}} + :#Radix {:description "https://www.radix-ui.com/primitives/docs/components/toast#root" + :control {:hideNoControlsWarning true} + :table {:category :more + :type {:detail nil}}}} + + :args {:title "" + :description "This is description content" + :variant :default + :duration 3000}}) + +(defstory ImperativeAPI + {:render + (rum/defc Toaster [props] + [:<> + [:p.flex.space-x-3 + ;; basic + (ui/button + {:on-click + #(ui/toast! + [:b (:description props)] + (:variant props) + {:title (:title props) + :duration (:duration props)})} + "open default toast") + + ;; update + (ui/button + {:class "primary-yellow" + :on-click #(ui/toast! + (fn [{:keys [dismiss! update!]}] + [:div + [:p.text-6xl.text-green-500 "toast content..."] + [:p.pt-4.space-x-2.flex + (ui/button + {:variant :destructive :size :sm :on-click dismiss!} + ":handle close") + (ui/button + {:size :sm + :on-click (fn [] (update! {:title [:b.text-2xl (js/Date.now)]}))} + ":handle update")]]))} + "open callback toast") + + ;; clear all + (ui/button + {:variant :destructive + :on-click #(ui/toast-dismiss!)} + (ui/tabler-icon "x") "dismiss all")] + + ;; install toaster + (toaster/install-toaster) + ])}) diff --git a/deps/shui/src/logseq/shui/storybook.clj b/deps/shui/src/logseq/shui/storybook.clj new file mode 100644 index 00000000000..1b6e3fce851 --- /dev/null +++ b/deps/shui/src/logseq/shui/storybook.clj @@ -0,0 +1,32 @@ +(ns logseq.shui.storybook) + +(defmacro defmeta + [title configs] + `(def ~(with-meta + 'default + {:export true}) + (let [ret# ~(assoc configs :title (-> (str title) (clojure.string/replace ":" ""))) + cp# (:component ret#) + ret# (cond-> ret# + (fn? cp#) + (assoc :component + (fn [^js args#] + (let [{:keys [~'children] :as args#} (cljs-bean.core/->clj args#)] + (cp# (dissoc args# :children) + (if (fn? ~'children) (~'children) ~'children))))))] + (cljs-bean.core/->js ret#)))) + +(defmacro defstory + [title configs] + `(def ~(with-meta (symbol (name title)) {:export true}) + (let [ret# ~configs + render# (:render ret#) + ret# (cond-> ret# + (fn? render#) + (assoc :render + (fn [^js args#] + (let [{:keys [~'children] :as args#} (cljs-bean.core/->clj args#)] + (let [~'res (render# (dissoc args# :children) + (if (fn? ~'children) (~'children) ~'children))] + (daiquiri.interpreter/interpret ~'res))))))] + (cljs-bean.core/->js ret#)))) \ No newline at end of file diff --git a/deps/shui/src/logseq/shui/storybook.cljs b/deps/shui/src/logseq/shui/storybook.cljs new file mode 100644 index 00000000000..215b39942c5 --- /dev/null +++ b/deps/shui/src/logseq/shui/storybook.cljs @@ -0,0 +1,7 @@ +(ns logseq.shui.storybook + (:require [logseq.shui.stories.button-story] + [logseq.shui.stories.toaster-story] + [logseq.shui.stories.badge-story] + [logseq.shui.ui])) + +(prn "[shui storybook] init") \ No newline at end of file diff --git a/deps/shui/src/logseq/shui/toaster/core.cljs b/deps/shui/src/logseq/shui/toaster/core.cljs new file mode 100644 index 00000000000..4115a1ddb17 --- /dev/null +++ b/deps/shui/src/logseq/shui/toaster/core.cljs @@ -0,0 +1,72 @@ +(ns logseq.shui.toaster.core + (:require [rum.core :as rum] + [daiquiri.interpreter :refer [interpret]] + [logseq.shui.util :as util] + [cljs-bean.core :as bean])) + +(defonce ^:private Toaster (util/lsui-wrap "Toaster")) +(defonce ^:private *toast (atom nil)) + +(defn gen-id [] + (js/window.LSUI.genToastId)) + +(defn use-toast [] + (when-let [^js js-toast (js/window.LSUI.useToast)] + (let [toast-fn! (.-toast js-toast) + dismiss! (.-dismiss js-toast)] + [(fn [s] + (let [^js s (bean/->js s)] + (toast-fn! s))) + dismiss!]))) + +(rum/defc install-toaster + < rum/static + [] + (let [^js js-toast (js/window.LSUI.useToast)] + (rum/use-effect! + (fn [] + (reset! *toast {:toast (.-toast js-toast) + :dismiss (.-dismiss js-toast) + :update (.-update js-toast)}) + #()) + []) + [:<> (Toaster)])) + +(defn update-html-props + [v] + (update-keys v + #(case % + :class :className + :for :htmlFor + %))) + +(defn interpret-vals + [config ks & args] + (reduce (fn [config k] + (let [v (get config k) + v (if (fn? v) (apply v args) v)] + (if (vector? v) (assoc config k (interpret v)) config))) + config ks)) + +(defn toast! + ([content-or-config] (toast! content-or-config :default nil)) + ([content-or-config status] (toast! content-or-config status nil)) + ([content-or-config status opts] + (if-let [{:keys [toast dismiss]} @*toast] + (let [config (if (map? content-or-config) + content-or-config + (-> {:description content-or-config} + (merge (if (map? status) status {:variant status})))) + config (update-html-props (merge config opts)) + id (or (:id config) (gen-id)) + config (assoc config :id id) + config (interpret-vals config [:title :description :action :icon] + {:id id :dismiss! #(dismiss id) :update! #(toast! (assoc %1 :id id))})] + (js->clj (toast (clj->js config)))) + :exception))) + +(defn dismiss! + ([] (dismiss! nil)) + ([id] + (when-let [{:keys [dismiss]} @*toast] + (dismiss id)))) diff --git a/deps/shui/src/logseq/shui/ui.cljs b/deps/shui/src/logseq/shui/ui.cljs new file mode 100644 index 00000000000..be5fed42483 --- /dev/null +++ b/deps/shui/src/logseq/shui/ui.cljs @@ -0,0 +1,105 @@ +(ns logseq.shui.ui + (:require [logseq.shui.util :as util] + [logseq.shui.icon.v2 :as icon-v2] + [logseq.shui.toaster.core :as toaster-core] + [logseq.shui.select.core :as select-core] + [logseq.shui.dialog.core :as dialog-core] + [logseq.shui.form.core :as form-core])) + +(def button (util/lsui-wrap "Button" {:static? false})) +(def link (util/lsui-wrap "Link")) +(def tabler-icon icon-v2/root) + +(def alert (util/lsui-wrap "Alert")) +(def alert-title (util/lsui-wrap "AlertTitle")) +(def alert-description (util/lsui-wrap "AlertDescription")) +(def slider (util/lsui-wrap "Slider")) +(def badge (util/lsui-wrap "Badge")) +(def input (util/lsui-wrap "Input")) +(def textarea (util/lsui-wrap "Textarea")) +(def switch (util/lsui-wrap "Switch")) +(def checkbox (util/lsui-wrap "Checkbox")) +(def radio-group (util/lsui-wrap "RadioGroup")) +(def radio-group-item (util/lsui-wrap "RadioGroupItem")) +(def skeleton (util/lsui-wrap "Skeleton")) +(def calendar (util/lsui-wrap "Calendar")) +(def popover (util/lsui-wrap "Popover")) +(def popover-trigger (util/lsui-wrap "PopoverTrigger")) +(def popover-content (util/lsui-wrap "PopoverContent")) + +(def tooltip (util/lsui-wrap "Tooltip")) +(def tooltip-trigger (util/lsui-wrap "TooltipTrigger")) +(def tooltip-content (util/lsui-wrap "TooltipContent")) +(def tooltip-provider (util/lsui-wrap "TooltipProvider")) + +(def card (util/lsui-wrap "Card")) +(def card-header (util/lsui-wrap "CardHeader")) +(def card-title (util/lsui-wrap "CardTitle")) +(def card-description (util/lsui-wrap "CardDescription")) +(def card-content (util/lsui-wrap "CardContent")) +(def card-footer (util/lsui-wrap "CardFooter")) + +(def form-provider form-core/form-provider) +(def form-item form-core/form-item) +(def form-label form-core/form-label) +(def form-description form-core/form-description) +(def form-message form-core/form-message) +(def form-field form-core/form-field) +(def form-control form-core/form-control) + +(def select select-core/select) +(def select-group select-core/select-group) +(def select-value select-core/select-value) +(def select-trigger select-core/select-trigger) +(def select-content select-core/select-content) +(def select-label select-core/select-label) +(def select-item select-core/select-item) +(def select-separator select-core/select-separator) +(def select-scroll-up-button select-core/select-scroll-up-button) +(def select-scroll-down-button select-core/select-scroll-down-button) + +(def dropdown-menu (util/lsui-wrap "DropdownMenu")) +(def dropdown-menu-trigger (util/lsui-wrap "DropdownMenuTrigger")) +(def dropdown-menu-content (util/lsui-wrap "DropdownMenuContent")) +(def dropdown-menu-group (util/lsui-wrap "DropdownMenuGroup")) +(def dropdown-menu-item (util/lsui-wrap "DropdownMenuItem")) +(def dropdown-menu-checkbox-item (util/lsui-wrap "DropdownMenuCheckboxItem")) +(def dropdown-menu-radio-group (util/lsui-wrap "DropdownMenuRadioGroup")) +(def dropdown-menu-radio-item (util/lsui-wrap "DropdownMenuRadioItem")) +(def dropdown-menu-label (util/lsui-wrap "DropdownMenuLabel")) +(def dropdown-menu-separator (util/lsui-wrap "DropdownMenuSeparator")) +(def dropdown-menu-shortcut (util/lsui-wrap "DropdownMenuShortcut")) +(def dropdown-menu-portal (util/lsui-wrap "DropdownMenuPortal")) +(def dropdown-menu-sub (util/lsui-wrap "DropdownMenuSub")) +(def dropdown-menu-sub-content (util/lsui-wrap "DropdownMenuSubContent")) +(def dropdown-menu-sub-trigger (util/lsui-wrap "DropdownMenuSubTrigger")) + +(def context-menu (util/lsui-wrap "ContextMenu")) +(def context-menu-trigger (util/lsui-wrap "ContextMenuTrigger")) +(def context-menu-content (util/lsui-wrap "ContextMenuContent")) +(def context-menu-item (util/lsui-wrap "ContextMenuItem")) +(def context-menu-checkbox-item (util/lsui-wrap "ContextMenuCheckboxItem")) +(def context-menu-radio-item (util/lsui-wrap "ContextMenuRadioItem")) +(def context-menu-label (util/lsui-wrap "ContextMenuLabel")) +(def context-menu-separator (util/lsui-wrap "ContextMenuSeparator")) +(def context-menu-shortcut (util/lsui-wrap "ContextMenuShortcut")) +(def context-menu-group (util/lsui-wrap "ContextMenuGroup")) +(def context-menu-portal (util/lsui-wrap "ContextMenuPortal")) +(def context-menu-sub (util/lsui-wrap "ContextMenuSub")) +(def context-menu-sub-content (util/lsui-wrap "ContextMenuSubContent")) +(def context-menu-sub-trigger (util/lsui-wrap "ContextMenuSubTrigger")) +(def context-menu-radio-group (util/lsui-wrap "ContextMenuRadioGroup")) + +(def dialog dialog-core/dialog) +(def dialog-portal dialog-core/dialog-portal) +(def dialog-overlay dialog-core/dialog-overlay) +(def dialog-close dialog-core/dialog-close) +(def dialog-trigger dialog-core/dialog-trigger) +(def dialog-content dialog-core/dialog-content) +(def dialog-header dialog-core/dialog-header) +(def dialog-footer dialog-core/dialog-footer) +(def dialog-title dialog-core/dialog-title) +(def dialog-description dialog-core/dialog-description) + +(def toast! toaster-core/toast!) +(def toast-dismiss! toaster-core/dismiss!) diff --git a/deps/shui/src/logseq/shui/util.cljs b/deps/shui/src/logseq/shui/util.cljs index c0e33fac77f..fa4c8c5b916 100644 --- a/deps/shui/src/logseq/shui/util.cljs +++ b/deps/shui/src/logseq/shui/util.cljs @@ -1,9 +1,15 @@ (ns logseq.shui.util - (:require - [clojure.string :as s] - [rum.core :refer [use-state use-effect!] :as rum] - [goog.dom :as gdom])) + (:require + [clojure.string :as s] + [rum.core :refer [use-state use-effect!] :as rum] + [logseq.shui.rum :as shui-rum] + [goog.object :refer [getValueByKeys] :as gobj] + [clojure.set :refer [rename-keys]] + [clojure.walk :as w] + [cljs-bean.core :as bean] + [goog.dom :as gdom])) +(goog-define NODETEST false) ;; /--------------- app ------------\ ;; /-------- left --------\ \ @@ -16,48 +22,48 @@ ;; | | | | ;; |--------|-------------------|-------------| -(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 $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) + {:top (.-top rect) + :left (.-left rect) :bottom (.-bottom rect) - :right (.-right rect) - :width (.-width rect) + :right (.-right rect) + :width (.-width rect) :height (.-height rect) - :x (.-x rect) - :y (.-y rect)})) + :x (.-x rect) + :y (.-y rect)})) (defn clj-rect-observer [update!] (js/ResizeObserver. - (fn [entries] + (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] + ([el tick] (let [[rect set-rect] (rum/use-state nil)] - (rum/use-effect! - (if el - (fn [] + (rum/use-effect! + (if el + (fn [] (let [update! #(set-rect (el->clj-rect el)) observer (clj-rect-observer update!)] (update!) - (.observe observer el) + (.observe observer el) #(.disconnect observer))) #()) [el tick]) rect))) - -(defn use-ref-bounding-client-rect + +(defn use-ref-bounding-client-rect ([] (use-ref-bounding-client-rect nil)) ([tick] (let [[ref set-ref] (rum/use-state nil) @@ -65,17 +71,127 @@ [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))) + js/getComputedStyle + (.-fontSize) + (js/parseFloat) + (* rem))) (defn px->rem [px] (->> js/document.documentElement - js/getComputedStyle - (.-fontSize) - (js/parseFloat) - (/ px))) + js/getComputedStyle + (.-fontSize) + (js/parseFloat) + (/ px))) + +(defn kebab-case->camel-case + "Converts from kebab case to camel case, eg: on-click to onClick" + [input] + (let [words (s/split input #"-") + capitalize (->> (rest words) + (map #(apply str (s/upper-case (first %)) (rest %))))] + (apply str (first words) capitalize))) + +(defn map-keys->camel-case + "Stringify all the keys of a cljs hashmap and converts them + from kebab case to camel case. If :html-props option is specified, + then rename the html properties values to their dom equivalent + before conversion" + [data & {:keys [html-props]}] + (let [convert-to-camel (fn [[key value]] + (let [k (name key)] + [(if-not (s/starts-with? k "data-") + (kebab-case->camel-case k) k) value]))] + (w/postwalk (fn [x] + (if (map? x) + (let [new-map (if html-props + (rename-keys x {:class :className :for :htmlFor}) + x)] + (into {} (map convert-to-camel new-map))) + x)) + data))) + +(def dev? (some-> (aget js/window "LSUtils") (aget "isDev"))) + +(defn get-path + "Returns the component path." + [component-name] + (s/split (name component-name) #"\.")) + +(defn adapt-class [react-class & args] + (let [[opts children] (if (map? (first args)) + [(first args) (rest args)] + [{} args]) + type# (first children) + ;; we have to make sure to check if the children is sequential + ;; as a list can be returned, eg: from a (for) + new-children (if (sequential? type#) + [(daiquiri.interpreter/interpret children)] + (daiquiri.interpreter/interpret children)) + ;; convert any options key value to a React element, if + ;; a valid html element tag is used, using sablono (rum.daiquiri) + vector->react-elems (fn [[key val]] + (if (sequential? val) + [key (daiquiri.interpreter/interpret val)] + [key val])) + new-options (into {} (map vector->react-elems opts)) + react-class (if dev? (react-class) react-class)] + (apply js/React.createElement react-class + ;; sablono html-to-dom-attrs does not work for nested hash-maps + (bean/->js (map-keys->camel-case new-options :html-props true)) + new-children))) + +(defn use-atom-fn + [a getter-fn setter-fn] + (let [[val set-val] (use-state (getter-fn @a))] + (use-effect! + (fn [] + (let [id (str (random-uuid))] + (add-watch a id (fn [_ _ prev-state next-state] + (let [prev-value (getter-fn prev-state) + next-value (getter-fn next-state)] + (when-not (= prev-value next-value) + (set-val next-value))))) + #(remove-watch a id))) + []) + [val #(swap! a setter-fn %)])) + +(defn use-atom + "(use-atom my-atom)" + [a] + (use-atom-fn a identity (fn [_ v] v))) + +(defn use-mounted + [] + (let [*mounted (rum/use-ref false)] + (use-effect! + (fn [] + (rum/set-ref! *mounted true) + #(rum/set-ref! *mounted false)) + []) + #(rum/deref *mounted))) + +(defn react->rum [c static?] + (if static? + (rum/defc react->rum' < rum/static + [& args] + (apply adapt-class c args)) + (partial adapt-class c))) + +(defn component-wrap + "Returns the component by the given component name." + [^js ns name & {:keys [static?] :or {static? false}}] + (let [path (get-path name) + ;; lazy calculating is for HMR from ts + cp #(gobj/getValueByKeys ns (clj->js path))] + (react->rum (if dev? cp (cp)) static?))) + +(def lsui-wrap + (partial component-wrap js/window.LSUI)) + +(defn lsui-get + [name] + (if NODETEST + #js {} + (aget js/window.LSUI name))) diff --git a/e2e-tests/accessibility.spec.ts b/e2e-tests/accessibility.spec.ts index b78cde0dfae..aa902a2aad7 100644 --- a/e2e-tests/accessibility.spec.ts +++ b/e2e-tests/accessibility.spec.ts @@ -3,7 +3,8 @@ import { createRandomPage } from './utils' import { expect } from '@playwright/test' import AxeBuilder from '@axe-core/playwright' -test('should not have any automatically detectable accessibility issues', async ({ page }) => { +// TODO: more configuration is required for this test +test.skip('should not have any automatically detectable accessibility issues', async ({ page }) => { try { await page.waitForSelector('.notification-clear', { timeout: 10 }) page.click('.notification-clear') diff --git a/e2e-tests/basic.spec.ts b/e2e-tests/basic.spec.ts index 6d835a0e0dc..04cf07f2615 100644 --- a/e2e-tests/basic.spec.ts +++ b/e2e-tests/basic.spec.ts @@ -85,33 +85,79 @@ test('delete and backspace', async ({ page, block }) => { }) -test('selection', async ({ page, block }) => { +test('block selection', async ({ page, block }) => { await createRandomPage(page) - // add 5 blocks - await block.mustFill('line 1') + await block.mustFill('1') await block.enterNext() - await block.mustFill('line 2') + await block.mustFill('2') + expect(await block.indent()).toBe(true) + await block.enterNext() + await block.mustFill('3') + await block.enterNext() + await block.mustFill('4') + expect(await block.unindent()).toBe(true) await block.enterNext() + await block.mustFill('5') expect(await block.indent()).toBe(true) - await block.mustFill('line 3') await block.enterNext() - await block.mustFill('line 4') + await block.mustFill('6') + await block.enterNext() + await block.mustFill('7') + expect(await block.unindent()).toBe(true) + await block.enterNext() + await block.mustFill('8') expect(await block.indent()).toBe(true) await block.enterNext() - await block.mustFill('line 5') + await block.mustFill('9') + expect(await block.unindent()).toBe(true) - // shift+up select 3 blocks + // shift+up/down await page.keyboard.down('Shift') await page.keyboard.press('ArrowUp') + await block.waitForSelectedBlocks(1) + let locator = page.locator('.ls-block >> nth=8') + await page.keyboard.press('ArrowUp') + await block.waitForSelectedBlocks(2) + await page.keyboard.press('ArrowUp') + await block.waitForSelectedBlocks(3) + + await page.keyboard.press('ArrowDown') + await block.waitForSelectedBlocks(2) await page.keyboard.up('Shift') + // mod+click select or deselect + await page.keyboard.down(modKey) + await page.click('.ls-block >> nth=7') + await block.waitForSelectedBlocks(1) + + await page.click('.block-main-container >> nth=6') + await block.waitForSelectedBlocks(2) + + // mod+shift+click + await page.click('.ls-block >> nth=4') await block.waitForSelectedBlocks(3) - await page.keyboard.press('Backspace') - await block.waitForBlocks(2) + await page.keyboard.down('Shift') + await page.click('.ls-block >> nth=1') + await block.waitForSelectedBlocks(6) + + await page.keyboard.up('Shift') + await page.keyboard.up(modKey) + await page.keyboard.press('Escape') + + // shift+click + await page.keyboard.down('Shift') + await page.click('.block-main-container >> nth=0') + await page.click('.block-main-container >> nth=3') + await block.waitForSelectedBlocks(4) + await page.click('.ls-block >> nth=8') + await block.waitForSelectedBlocks(9) + await page.click('.ls-block >> nth=5') + await block.waitForSelectedBlocks(6) + await page.keyboard.up('Shift') }) test('template', async ({ page, block }) => { @@ -120,7 +166,7 @@ test('template', async ({ page, block }) => { await createRandomPage(page) await block.mustFill('template test\ntemplate:: ') - await page.keyboard.type(randomTemplate, {delay: 100}) + await page.keyboard.type(randomTemplate, { delay: 100 }) await page.keyboard.press('Enter') await block.clickNext() @@ -242,7 +288,7 @@ test('invalid page props #3944', async ({ page, block }) => { await block.enterNext() }) -test('Scheduled date picker should point to the already specified Date #6985', async({page,block})=>{ +test('Scheduled date picker should point to the already specified Date #6985', async ({ page, block }) => { await createRandomPage(page) await block.mustFill('testTask \n SCHEDULED: <2000-05-06 Sat>') @@ -253,15 +299,15 @@ test('Scheduled date picker should point to the already specified Date #6985', a // Open date picker await page.click('a.opacity-80') await page.waitForTimeout(500) - expect(page.locator('text=May 2000')).toBeVisible() - expect(page.locator('td:has-text("6").active')).toBeVisible() + await expect(page.locator('text=May 2000')).toBeVisible() + await expect(page.locator('td:has-text("6").active')).toBeVisible() // Close date picker await page.click('a.opacity-80') await page.waitForTimeout(500) }) -test('Opening a second datepicker should close the first one #7341', async({page,block})=>{ +test('Opening a second datepicker should close the first one #7341', async ({ page, block }) => { await createRandomPage(page) await block.mustFill('testTask \n SCHEDULED: <2000-05-06 Sat>') @@ -279,10 +325,10 @@ test('Opening a second datepicker should close the first one #7341', async({page await page.waitForTimeout(50) await page.click('a:has-text("2000-05-06 Sat").opacity-80') await page.waitForTimeout(50) - expect(page.locator('text=May 2000')).toBeVisible() - expect(page.locator('td:has-text("6").active')).toBeVisible() - expect(page.locator('text=June 2000')).not.toBeVisible() - expect(page.locator('td:has-text("7").active')).not.toBeVisible() + await expect(page.locator('text=May 2000')).toBeVisible() + await expect(page.locator('td:has-text("6").active')).toBeVisible() + await expect(page.locator('text=June 2000')).not.toBeVisible() + await expect(page.locator('td:has-text("7").active')).not.toBeVisible() // Close date picker await page.click('a:has-text("2000-05-06 Sat").opacity-80') diff --git a/e2e-tests/page-rename.spec.ts b/e2e-tests/page-rename.spec.ts index 95bab318341..90bb80a21e3 100644 --- a/e2e-tests/page-rename.spec.ts +++ b/e2e-tests/page-rename.spec.ts @@ -91,7 +91,7 @@ test('page title property test', async ({ page }) => { await page.type(':nth-match(textarea, 1)', 'title:: ' + new_name + " ") await page.press(':nth-match(textarea, 1)', 'Enter') // DWIM property mode creates new line await page.press(':nth-match(textarea, 1)', 'Enter') - expect(await page.innerText('.page-title .title')).toBe(new_name) + await expect(page.locator('.page-title .title')).toHaveText(new_name) // Edit Title Property and Esc (ETPE) // exit editing via moving out focus @@ -101,5 +101,5 @@ test('page title property test', async ({ page }) => { await createPage(page, original_name) await page.type(':nth-match(textarea, 1)', 'title:: ' + new_name) await page.press(':nth-match(textarea, 1)', 'Escape') - expect(await page.innerText('.page-title .title')).toBe(new_name) + await expect(page.locator('.page-title .title')).toHaveText(new_name) }) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 060ca747687..083725ecf6f 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -519,7 +519,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.10.3; + MARKETING_VERSION = 0.10.4; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -546,7 +546,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.10.3; + MARKETING_VERSION = 0.10.4; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -571,7 +571,7 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 0.10.3; + MARKETING_VERSION = 0.10.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController; @@ -598,7 +598,7 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 0.10.3; + MARKETING_VERSION = 0.10.4; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/libs/package.json b/libs/package.json index d07348a6183..2ebfc7e7921 100644 --- a/libs/package.json +++ b/libs/package.json @@ -1,6 +1,6 @@ { "name": "@logseq/libs", - "version": "0.0.16", + "version": "0.0.17", "description": "Logseq SDK libraries", "main": "dist/lsplugin.user.js", "typings": "index.d.ts", diff --git a/libs/src/LSPlugin.caller.ts b/libs/src/LSPlugin.caller.ts index b23aafa5a72..d30294a634f 100644 --- a/libs/src/LSPlugin.caller.ts +++ b/libs/src/LSPlugin.caller.ts @@ -286,7 +286,7 @@ class LSPluginCaller extends EventEmitter { timer = setTimeout(() => { reject(new Error(`handshake Timeout`)) pt.destroy() - }, 4 * 1000) // 4 secs + }, 8 * 1000) // 8 secs handshake .then((refChild: ParentAPI) => { diff --git a/libs/src/LSPlugin.core.ts b/libs/src/LSPlugin.core.ts index 9bf7ae3db03..92f57ba6eb0 100644 --- a/libs/src/LSPlugin.core.ts +++ b/libs/src/LSPlugin.core.ts @@ -384,14 +384,14 @@ function convertToLSPResource(fullUrl: string, dotPluginRoot: string) { class IllegalPluginPackageError extends Error { constructor(message: string) { super(message) - this.name = IllegalPluginPackageError.name + this.name = 'IllegalPluginPackageError' } } class ExistedImportedPluginPackageError extends Error { constructor(message: string) { super(message) - this.name = ExistedImportedPluginPackageError.name + this.name = 'ExistedImportedPluginPackageError' } } @@ -409,7 +409,7 @@ class PluginLocal extends EventEmitter< private _localRoot?: string private _dotSettingsFile?: string private _caller?: LSPluginCaller - private _logger?: PluginLogger + private _logger?: PluginLogger = new PluginLogger('PluginLocal') /** * @param _options @@ -595,9 +595,7 @@ class PluginLocal extends EventEmitter< // Validate id const { registeredPlugins, isRegistering } = this._ctx if (isRegistering && registeredPlugins.has(this.id)) { - throw new ExistedImportedPluginPackageError( - 'Registered plugin package Error' - ) + throw new ExistedImportedPluginPackageError(this.id) } return async () => { diff --git a/libs/src/LSPlugin.ts b/libs/src/LSPlugin.ts index e17bd8e00ce..c3410070748 100644 --- a/libs/src/LSPlugin.ts +++ b/libs/src/LSPlugin.ts @@ -141,7 +141,7 @@ export interface AppUserInfo { export interface AppInfo { version: string - [key: string]: any + [key: string]: unknown } /** @@ -159,6 +159,8 @@ export interface AppUserConfigs { showBracket: boolean enabledFlashcards: boolean enabledJournals: boolean + + [key: string]: unknown } /** @@ -168,6 +170,8 @@ export interface AppGraphInfo { name: string url: string path: string + + [key: string]: unknown } /** @@ -193,6 +197,8 @@ export interface BlockEntity { meta?: { timestamps: any; properties: any; startPos: number; endPos: number } title?: Array marker?: string + + [key: string]: unknown } /** @@ -212,6 +218,8 @@ export interface PageEntity { format?: 'markdown' | 'org' journalDay?: number updatedAt?: number + + [key: string]: unknown } export type BlockIdentity = BlockUUID | Pick diff --git a/package.json b/package.json index 946ab05ad3a..401670cfb23 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "stylelint": "^13.8.0", "stylelint-config-standard": "^20.0.0", "tailwindcss": "3.3.5", + "tailwindcss-animate": "^1.0.7", "typescript": "^4.4.3" }, "scripts": { @@ -56,6 +57,7 @@ "css:build": "postcss tailwind.all.css -o static/css/style.css --verbose --env production", "css:watch": "cross-env TAILWIND_MODE=watch postcss tailwind.all.css -o static/css/style.css --verbose --watch", "cljs:watch": "clojure -M:cljs watch app electron", + "cljs:watch-storybook": "clojure -M:cljs watch stories-dev", "cljs:app-watch": "clojure -M:cljs watch app", "cljs:electron-watch": "clojure -M:cljs watch app electron --config-merge \"{:asset-path \\\"./js\\\"}\"", "cljs:release": "clojure -M:cljs release app publishing electron", diff --git a/packages/amplify/yarn.lock b/packages/amplify/yarn.lock index 92c4a99bd3f..376f5801834 100644 --- a/packages/amplify/yarn.lock +++ b/packages/amplify/yarn.lock @@ -2461,35 +2461,35 @@ "@lezer/lr" "^0.15.4" json5 "^2.2.1" -"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.0.tgz#d31a238c943ffc34bab73ad6ce7a6466d65888ef" - integrity sha512-5qpnNHUyyEj9H3sm/4Um/bnx1lrQGhe8iqry/1d+cQYCRd/gzYA0YLeq0ezlk4hKx4vO+dsEsNyeowqRqslwQA== +"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38" + integrity sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ== -"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.0.tgz#2f6fbbec3d3f0bbe9c6678c899f1c1a6e25ed980" - integrity sha512-ZphTFFd6SFweNAMKD+QJCrWpgkjf4qBuHltiMkKkD6FFrB3NOTRVmetAGTkJ57pa+s6J0yCH06LujWB9rZe94g== +"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz#f954f34355712212a8e06c465bc06c40852c6bb3" + integrity sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw== -"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.0.tgz#19875441da50b9aa8f8e726eb097a4cead435a3f" - integrity sha512-NEX6hdSvP4BmVyegaIbrGxvHzHvTzzsPaxXCsUt0mbLbPpEftsvNwaEVKOowXnLoeuGeD4MaqSwL3BUK2elsUA== +"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz#45c63037f045c2b15c44f80f0393fa24f9655367" + integrity sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg== -"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.0.tgz#3b855ac72cc16e89db2f72adf47ddc964c20a53d" - integrity sha512-ztKVV1dO/sSZyGse0PBCq3Pk1PkYjsA/dsEWE7lfrGoAK3i9HpS2o7XjGQ7V4va6nX+xPPOiuYpQwa4Bi6vlww== +"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz#35707efeafe6d22b3f373caf9e8775e8920d1399" + integrity sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA== -"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.0.tgz#455f1d5bb00e87f78c67711f26e7bff9f1457684" - integrity sha512-9uvdAkZMOPCY7SPRxZLW8XGqBOVNVEhqlgffenN8shA1XR9FWVsSM13nr/oHtNgXg6iVyML7RwWPyqUeThlwxg== +"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz#091b1218b66c341f532611477ef89e83f25fae4f" + integrity sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA== -"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.0.tgz#03c6bfcd3acb179ea69546c20d50895b9d623ada" - integrity sha512-Wg0+9615kHKlr9iLVcG5I+/CHnf6w3x5UADRv8Ad16yA0Bu5l9eVOROjV7aHPG6uC8ZPFIVVaoSjDChD+Y0pzg== +"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz#0f164b726869f71da3c594171df5ebc1c4b0a407" + integrity sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ== "@parcel/bundler-default@2.8.3": version "2.8.3" @@ -4113,9 +4113,9 @@ find-up@^4.1.0: path-exists "^4.0.0" follow-redirects@^1.14.8: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + version "1.15.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" + integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== fs-extra@^10.0.0: version "10.1.0" @@ -4616,26 +4616,26 @@ minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -msgpackr-extract@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.0.tgz#5b5c5fbfff25be5ee5b5a82a9cbe02e37f72bed0" - integrity sha512-oy6KCk1+X4Bn5m6Ycq5N1EWl9npqG/cLrE8ga8NX7ZqfqYUUBS08beCQaGq80fjbKBySur0E6x//yZjzNJDt3A== +msgpackr-extract@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz#e05ec1bb4453ddf020551bcd5daaf0092a2c279d" + integrity sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A== dependencies: node-gyp-build-optional-packages "5.0.7" optionalDependencies: - "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.0" - "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.0" - "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.0" - "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.0" - "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.0" - "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.0" + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.2" msgpackr@^1.5.4: - version "1.8.3" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.8.3.tgz#78c1b91359f72707f4abeaca40cc423bd2d75185" - integrity sha512-m2JefwcKNzoHYXkH/5jzHRxAw7XLWsAdvu0FOJ+OLwwozwOV/J6UA62iLkfIMbg7G8+dIuRwgg6oz+QoQ4YkoA== + version "1.10.1" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.10.1.tgz#51953bb4ce4f3494f0c4af3f484f01cfbb306555" + integrity sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ== optionalDependencies: - msgpackr-extract "^3.0.0" + msgpackr-extract "^3.0.2" murmurhash-js@^1.0.0: version "1.0.0" diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore new file mode 100644 index 00000000000..33e3e87ebd3 --- /dev/null +++ b/packages/ui/.gitignore @@ -0,0 +1,3 @@ +.parcel-cache +.storybook/cljs +dist \ No newline at end of file diff --git a/packages/ui/.postcssrc b/packages/ui/.postcssrc new file mode 100644 index 00000000000..02930d6b4b0 --- /dev/null +++ b/packages/ui/.postcssrc @@ -0,0 +1,6 @@ +{ + "plugins": { + "tailwindcss/nesting": {}, + "tailwindcss": {} + } +} \ No newline at end of file diff --git a/packages/ui/.storybook/main.js b/packages/ui/.storybook/main.js new file mode 100644 index 00000000000..8e9faebce96 --- /dev/null +++ b/packages/ui/.storybook/main.js @@ -0,0 +1,76 @@ +import { join, dirname, resolve } from 'path' + +/** + * This function is used to resolve the absolute path of a package. + * It is needed in projects that use Yarn PnP or are set up within a monorepo. + */ +function getAbsolutePath(value) { + return dirname(require.resolve(join(value, 'package.json'))) +} + +/** @type { import('@storybook/react-webpack5').StorybookConfig } */ +const config = { + stories: [ + './cljs/*_story.js', + '../src/**/*.story.@(js|jsx|mjs|ts|tsx)' + ], + addons: [ + getAbsolutePath('@storybook/addon-links'), + getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('@storybook/addon-onboarding'), + getAbsolutePath('@storybook/addon-interactions'), + getAbsolutePath('@storybook/addon-toolbars') + ], + framework: { + name: getAbsolutePath('@storybook/react-webpack5'), + options: {}, + }, + docs: { + autodocs: 'tag', + }, + features: { + storyStoreV7: false + }, + + async webpackFinal(config) { + // module name resolver + config.resolve.alias = { + '@/components': resolve(__dirname, '../@/components'), + '@/lib': resolve(__dirname, '../@/lib') + } + + // NOTE: Don't use .babelrc for this. Because the parcel bundler share + // the babel config with storybook webpack from root path. + const babelLoaderRule = config.module.rules.find( + (rule) => rule.test.toString() === /\.(mjs|tsx?|jsx?)$/.toString() + ) + + // babelLoaderRule.include?.push(__dirname) + const babelLoaderPresets = babelLoaderRule?.use[0].options.presets + babelLoaderPresets.unshift( + [require.resolve('@babel/preset-env'), { + 'targets': { + 'chrome': 100, + 'safari': 15, + 'firefox': 91 + } + }] + ) + babelLoaderPresets.push('@babel/preset-typescript') + + // postcss loader + config.module.rules.push({ + test: /\.css$/, + use: [ + { + loader: 'postcss-loader', + options: {}, + }, + ], + }) + + return config + } +} + +export default config diff --git a/packages/ui/.storybook/manager.js b/packages/ui/.storybook/manager.js new file mode 100644 index 00000000000..122e6603fcd --- /dev/null +++ b/packages/ui/.storybook/manager.js @@ -0,0 +1,30 @@ +import React from 'react' + +import { addons, types } from '@storybook/manager-api' +import { FORCE_RE_RENDER } from '@storybook/core-events' + +addons.register('my/toolbar', () => { + addons.add('my-toolbar-addon/toolbar', { + title: 'Example Storybook toolbar', + //👇 Sets the type of UI element in Storybook + type: types.TOOL, + //👇 Shows the Toolbar UI element if either the Canvas or Docs tab is active + match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + render: ({ active }) => { + const defaultTheme = window.localStorage.getItem('__ls-theme-color__') + return ( +
+ +
+ ) + }, + }) +}) \ No newline at end of file diff --git a/packages/ui/.storybook/preview-head.html b/packages/ui/.storybook/preview-head.html new file mode 100644 index 00000000000..d134c9e82ef --- /dev/null +++ b/packages/ui/.storybook/preview-head.html @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/ui/.storybook/preview.js b/packages/ui/.storybook/preview.js new file mode 100644 index 00000000000..4375df4e991 --- /dev/null +++ b/packages/ui/.storybook/preview.js @@ -0,0 +1,73 @@ +import '../src/radix.css' +import '../src/radix-hsl.css' +import './theme.css' +import '../src/index.css' +import { useEffect } from 'react' + +// require in this file to keep app state when HMR +const { setupGlobals } = require('../src') + +setupGlobals() + +// REPL +if (process.env.NODE_ENV !== 'production') { + require('./cljs/cljs_env') + require('./cljs/shadow.cljs.devtools.client.browser') +} + +function ThemeObserver( + { children } +) { + const theme = window.localStorage.getItem('__ls-theme-color__') + + useEffect(() => { + const html = document.documentElement + html.dataset.color = theme + return () => (delete html.dataset.theme) + }, [theme]) + + return ( +
+ {children} +
+ ) +} + +/** @type { import('@storybook/react').Preview } */ +const preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, + decorators: [ + (Story) => { + return ( + + + + ) + } + ], + globalTypes: { + theme: { + // description: 'Global theme for components', + // defaultValue: 'light', + // toolbar: { + // // The label to show for this toolbar item + // title: 'Theme', + // icon: 'circlehollow', + // // Array of plain string values or MenuItem shape (see below) + // items: ['light', 'dark'], + // // Change title based on selected value + // dynamicTitle: true, + // }, + }, + }, +} + +export default preview diff --git a/packages/ui/.storybook/theme.css b/packages/ui/.storybook/theme.css new file mode 100644 index 00000000000..0ba9b38569b --- /dev/null +++ b/packages/ui/.storybook/theme.css @@ -0,0 +1,119 @@ +* { + @apply border-border; +} + +body { + @apply bg-background text-foreground; + + .ui__toaster { + &-viewport { + @apply gap-2; + } + } +} + +/* light */ +html { + &[data-color=default] { + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --primary: 200 97% 37%; + --primary-foreground: 255 92% 100%; + + --ring: 200 97% 37%; + } + + &[data-color=blue] { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --radius: 0.5rem; + } + + &[data-color=green] { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 142.1 76.2% 36.3%; + --primary-foreground: 355.7 100% 97.3%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 142.1 76.2% 36.3%; + --radius: 0.5rem; + } + + &[data-color=orange] { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + --primary: 24.6 95% 53.1%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 24.6 95% 53.1%; + --radius: 0.5rem; + } +} + +html[data-color-scheme=dark] { + &[data-color=green] { + --background: 20 14.3% 4.1%; + --foreground: 0 0% 95%; + --card: 24 9.8% 10%; + --card-foreground: 0 0% 95%; + --popover: 0 0% 9%; + --popover-foreground: 0 0% 95%; + --primary: 142.1 70.6% 45.3%; + --primary-foreground: 144.9 80.4% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 15%; + --muted-foreground: 240 5% 64.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 85.7% 97.3%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 142.4 71.8% 29.2%; + } +} diff --git a/packages/ui/.storybook/theme_hsl.css b/packages/ui/.storybook/theme_hsl.css new file mode 100644 index 00000000000..77344ec93f9 --- /dev/null +++ b/packages/ui/.storybook/theme_hsl.css @@ -0,0 +1,113 @@ +* { + @apply border-border; +} + +body { + @apply bg-background text-foreground; +} + +/* light */ +html { + &[data-theme=default] { + --accent: hsl(210 40% 96.1%); + --accent-foreground: hsl(222.2 47.4% 11.2%); + + --primary: hsl(200 97% 37%); + --primary-foreground: hsl(255 92% 100%); + + --ring: hsl(200 97% 37%); + } + + &[data-theme=blue] { + --background: hsl(0 0% 100%); + --foreground: hsl(222.2 84% 4.9%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(222.2 84% 4.9%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(222.2 84% 4.9%); + --primary: hsl(221.2 83.2% 53.3%); + --primary-foreground: hsl(210 40% 98%); + --secondary: hsl(210 40% 96.1%); + --secondary-foreground: hsl(222.2 47.4% 11.2%); + --muted: hsl(210 40% 96.1%); + --muted-foreground: hsl(215.4 16.3% 46.9%); + --accent: hsl(210 40% 96.1%); + --accent-foreground: hsl(222.2 47.4% 11.2%); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: hsl(210 40% 98%); + --border: hsl(214.3 31.8% 91.4%); + --input: hsl(214.3 31.8% 91.4%); + --ring: hsl(221.2 83.2% 53.3%); + --radius: 0.5rem; + } + + &[data-theme=green] { + --background: hsl(0 0% 100%); + --foreground: hsl(240 10% 3.9%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(240 10% 3.9%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(240 10% 3.9%); + --primary: hsl(142.1 76.2% 36.3%); + --primary-foreground: hsl(355.7 100% 97.3%); + --secondary: hsl(240 4.8% 95.9%); + --secondary-foreground: hsl(240 5.9% 10%); + --muted: hsl(240 4.8% 95.9%); + --muted-foreground: hsl(240 3.8% 46.1%); + --accent: hsl(240 4.8% 95.9%); + --accent-foreground: hsl(240 5.9% 10%); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 5.9% 90%); + --input: hsl(240 5.9% 90%); + --ring: hsl(142.1 76.2% 36.3%); + --radius: 0.5rem; + } + + &[data-theme=orange] { + --background: hsl(0 0% 100%); + --foreground: hsl(20 14.3% 4.1%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(20 14.3% 4.1%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(20 14.3% 4.1%); + --primary: hsl(24.6 95% 53.1%); + --primary-foreground: hsl(60 9.1% 97.8%); + --secondary: hsl(60 4.8% 95.9%); + --secondary-foreground: hsl(24 9.8% 10%); + --muted: hsl(60 4.8% 95.9%); + --muted-foreground: hsl(25 5.3% 44.7%); + --accent: hsl(60 4.8% 95.9%); + --accent-foreground: hsl(24 9.8% 10%); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: hsl(60 9.1% 97.8%); + --border: hsl(20 5.9% 90%); + --input: hsl(20 5.9% 90%); + --ring: hsl(24.6 95% 53.1%); + --radius: 0.5rem; + } +} + +html[data-color-scheme=dark] { + &[data-theme=green] { + --background: hsl(20 14.3% 4.1%); + --foreground: hsl(0 0% 95%); + --card: hsl(24 9.8% 10%); + --card-foreground: hsl(0 0% 95%); + --popover: hsl(0 0% 9%); + --popover-foreground: hsl(0 0% 95%); + --primary: hsl(142.1 70.6% 45.3%); + --primary-foreground: hsl(144.9 80.4% 10%); + --secondary: hsl(240 3.7% 15.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(0 0% 15%); + --muted-foreground: hsl(240 5% 64.9%); + --accent: hsl(12 6.5% 15.1%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 62.8% 30.6%); + --destructive-foreground: hsl(0 85.7% 97.3%); + --border: hsl(240 3.7% 15.9%); + --input: hsl(240 3.7% 15.9%); + --ring: hsl(142.4 71.8% 29.2%); + } +} diff --git a/packages/ui/@/components/ui/alert-dialog.tsx b/packages/ui/@/components/ui/alert-dialog.tsx new file mode 100644 index 00000000000..74bfc71c897 --- /dev/null +++ b/packages/ui/@/components/ui/alert-dialog.tsx @@ -0,0 +1,147 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '../../lib/utils' +import { buttonVariants } from './button' + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/packages/ui/@/components/ui/alert.tsx b/packages/ui/@/components/ui/alert.tsx new file mode 100644 index 00000000000..5fff67c8872 --- /dev/null +++ b/packages/ui/@/components/ui/alert.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +// @ts-ignore +import { cn } from '@/lib/utils' + +const alertVariants = cva( + 'ui__alert relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: + 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = 'Alert' + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = 'AlertTitle' + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = 'AlertDescription' + +export { Alert, AlertTitle, AlertDescription } diff --git a/packages/ui/@/components/ui/badge.tsx b/packages/ui/@/components/ui/badge.tsx new file mode 100644 index 00000000000..ea6887b43a3 --- /dev/null +++ b/packages/ui/@/components/ui/badge.tsx @@ -0,0 +1,37 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold ' + + 'transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-default', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground', + secondary: + 'border-transparent bg-secondary text-secondary-foreground', + destructive: + 'border-transparent bg-destructive text-destructive-foreground', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge ({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/packages/ui/@/components/ui/button.tsx b/packages/ui/@/components/ui/button.tsx new file mode 100644 index 00000000000..0abb2409a13 --- /dev/null +++ b/packages/ui/@/components/ui/button.tsx @@ -0,0 +1,69 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +// @ts-ignore +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm gap-1 ' + + 'font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 ' + + 'focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none ' + + 'disabled:opacity-50 select-none', + { + variants: { + variant: { + default: + 'bg-primary/90 hover:bg-primary/100 active:opacity-90 text-primary-foreground hover:text-primary-foreground as-classic', + solid: + 'bg-primary/90 hover:bg-primary/100 active:opacity-90 text-primary-foreground hover:text-primary-foreground as-solid', + destructive: + 'bg-destructive/90 hover:bg-destructive/100 active:opacity-90 text-destructive-foreground hover:text-destructive-foreground as-destructive', + outline: + 'border bg-background hover:bg-accent hover:text-accent-foreground active:opacity-80 as-outline', + secondary: + 'bg-secondary/70 text-secondary-foreground hover:bg-secondary/100 active:opacity-80 as-secondary', + ghost: + 'hover:bg-secondary/70 hover:text-secondary-foreground active:opacity-80 as-ghost', + link: + 'text-primary underline-offset-4 hover:underline active:opacity-80 as-link', + }, + size: { + default: 'h-10 px-4 py-2', + md: 'h-9 px-4 rounded-md py-2', + lg: 'h-11 text-base rounded-md px-8', + sm: 'h-7 rounded px-3 py-1', + xs: 'h-6 text-xs rounded px-3', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/packages/ui/@/components/ui/calendar.tsx b/packages/ui/@/components/ui/calendar.tsx new file mode 100644 index 00000000000..c08cbef02bc --- /dev/null +++ b/packages/ui/@/components/ui/calendar.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { DayPicker } from 'react-day-picker' + +// @ts-ignore +import { cn } from '@/lib/utils' +// @ts-ignore +import { buttonVariants } from '@/components/ui/button' + +export type CalendarProps = React.ComponentProps + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + , + IconRight: ({ ...props }) => , + }} + {...props} + /> + ) +} + +Calendar.displayName = 'Calendar' + +export { Calendar } diff --git a/packages/ui/@/components/ui/card.tsx b/packages/ui/@/components/ui/card.tsx new file mode 100644 index 00000000000..2e072fad815 --- /dev/null +++ b/packages/ui/@/components/ui/card.tsx @@ -0,0 +1,82 @@ +import * as React from 'react' + +// @ts-ignore +import { cn } from '@/lib/utils' + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/packages/ui/@/components/ui/checkbox.tsx b/packages/ui/@/components/ui/checkbox.tsx new file mode 100644 index 00000000000..87aa81e25a0 --- /dev/null +++ b/packages/ui/@/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from 'react' +import * as CheckboxPrimitive from '@radix-ui/react-checkbox' +import { Check } from 'lucide-react' + +// @ts-ignore +import { cn } from '@/lib/utils' + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/packages/ui/@/components/ui/command.tsx b/packages/ui/@/components/ui/command.tsx new file mode 100644 index 00000000000..d623ee06e92 --- /dev/null +++ b/packages/ui/@/components/ui/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/packages/ui/@/components/ui/context-menu.tsx b/packages/ui/@/components/ui/context-menu.tsx new file mode 100644 index 00000000000..be591ec7941 --- /dev/null +++ b/packages/ui/@/components/ui/context-menu.tsx @@ -0,0 +1,202 @@ +import * as React from 'react' +import * as ContextMenuPrimitive from '@radix-ui/react-context-menu' +import { Check, ChevronRight, Circle } from 'lucide-react' + +// @ts-ignore +import { cn } from '@/lib/utils' + +const ContextMenu = ContextMenuPrimitive.Root + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger + +const ContextMenuGroup = ContextMenuPrimitive.Group + +const ContextMenuPortal = ContextMenuPrimitive.Portal + +const ContextMenuSub = ContextMenuPrimitive.Sub + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean +} +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean +} +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean +} +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +ContextMenuShortcut.displayName = 'ContextMenuShortcut' + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/packages/ui/@/components/ui/dialog.tsx b/packages/ui/@/components/ui/dialog.tsx new file mode 100644 index 00000000000..d71a564c56c --- /dev/null +++ b/packages/ui/@/components/ui/dialog.tsx @@ -0,0 +1,132 @@ +import * as React from 'react' +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { X } from 'lucide-react' +import { cn } from '../../lib/utils' + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = 'DialogHeader' + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = 'DialogFooter' + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/packages/ui/@/components/ui/dropdown-menu.tsx b/packages/ui/@/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000000..bd01e54bb74 --- /dev/null +++ b/packages/ui/@/components/ui/dropdown-menu.tsx @@ -0,0 +1,204 @@ +import * as React from 'react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { Check, ChevronRight, Circle } from 'lucide-react' + +import { cn } from '@/lib/utils' + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean +} +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean +} +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean +} +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/packages/ui/@/components/ui/form.tsx b/packages/ui/@/components/ui/form.tsx new file mode 100644 index 00000000000..3019c543c80 --- /dev/null +++ b/packages/ui/@/components/ui/form.tsx @@ -0,0 +1,182 @@ +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, + useFormState, + useForm, +} from 'react-hook-form' + +// @ts-ignore +import { cn, useId } from '@/lib/utils' +import { Label } from './label' + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error('useFormField should be used within ') + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = useId() + + return ( + +
+ + ) +}) +FormItem.displayName = 'FormItem' + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +
+ + + + + + { + console.log('dd', item) + setQuery(item) + }} + placeholder="Search ..." + /> + No item found. + + {options.map((option) => ( + { + onChange( + selected.some((item) => item.value === option.value) + ? selected.filter((item) => item.value !== option.value) + : [...selected, option] + ) + setOpen(true) + }} + > + item.value === option.value) + ? 'opacity-100' + : 'opacity-0' + )} + /> + {option.label} + + ))} + + + + + ) + } +) + +MultiSelect.displayName = 'MultiSelect' + +export { MultiSelect } diff --git a/packages/ui/@/components/ui/popover.tsx b/packages/ui/@/components/ui/popover.tsx new file mode 100644 index 00000000000..3c0608e090a --- /dev/null +++ b/packages/ui/@/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' +import * as PopoverPrimitive from '@radix-ui/react-popover' + +// @ts-ignore +import { cn } from '@/lib/utils' + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/packages/ui/@/components/ui/radio-group.tsx b/packages/ui/@/components/ui/radio-group.tsx new file mode 100644 index 00000000000..de1a32930f5 --- /dev/null +++ b/packages/ui/@/components/ui/radio-group.tsx @@ -0,0 +1,44 @@ +import * as React from 'react' +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group' +import { Circle } from 'lucide-react' + +// @ts-ignore +import { cn } from '@/lib/utils' + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/packages/ui/@/components/ui/select.tsx b/packages/ui/@/components/ui/select.tsx new file mode 100644 index 00000000000..907eda8131f --- /dev/null +++ b/packages/ui/@/components/ui/select.tsx @@ -0,0 +1,166 @@ +import * as React from 'react' +import * as SelectPrimitive from '@radix-ui/react-select' +import { Check, ChevronDown, ChevronUp } from 'lucide-react' + +// @ts-ignore +import { cn } from '@/lib/utils' + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/packages/ui/@/components/ui/skeleton.tsx b/packages/ui/@/components/ui/skeleton.tsx new file mode 100644 index 00000000000..19bca8ad254 --- /dev/null +++ b/packages/ui/@/components/ui/skeleton.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' + +// @ts-ignore +import { cn } from '@/lib/utils' + +function Skeleton ({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/packages/ui/@/components/ui/slider.tsx b/packages/ui/@/components/ui/slider.tsx new file mode 100644 index 00000000000..174c7a94aa7 --- /dev/null +++ b/packages/ui/@/components/ui/slider.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import * as SliderPrimitive from '@radix-ui/react-slider' + +import { cn } from '@/lib/utils' + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)) +Slider.displayName = SliderPrimitive.Root.displayName + +export { Slider } diff --git a/packages/ui/@/components/ui/switch.tsx b/packages/ui/@/components/ui/switch.tsx new file mode 100644 index 00000000000..a0d4be153a7 --- /dev/null +++ b/packages/ui/@/components/ui/switch.tsx @@ -0,0 +1,37 @@ +import * as React from 'react' +import * as SwitchPrimitives from '@radix-ui/react-switch' + +// @ts-ignore +import { cn } from '@/lib/utils' + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { size?: string } +>(({ className, size, ...props }, ref) => { + const isSmall = size === 'sm' + return ( + + ) + } +) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/packages/ui/@/components/ui/textarea.tsx b/packages/ui/@/components/ui/textarea.tsx new file mode 100644 index 00000000000..9d973acca5d --- /dev/null +++ b/packages/ui/@/components/ui/textarea.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' + +// @ts-ignore +import { cn } from '@/lib/utils' + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +