mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 22:25:01 +00:00
* feat: use a button to load more journals * feat: preserve the "scroll to load more" behavior * ignore .clj-kondo all together * enhance: load latest two journals by default Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
622 lines
24 KiB
Clojure
622 lines
24 KiB
Clojure
(ns frontend.ui
|
|
(:require [rum.core :as rum]
|
|
[frontend.rum :as r]
|
|
["react-transition-group" :refer [TransitionGroup CSSTransition]]
|
|
["react-textarea-autosize" :as TextareaAutosize]
|
|
["react-resize-context" :as Resize]
|
|
[frontend.util :as util]
|
|
[frontend.mixins :as mixins]
|
|
[frontend.handler.notification :as notification-handler]
|
|
[frontend.state :as state]
|
|
[frontend.components.svg :as svg]
|
|
[clojure.string :as string]
|
|
[goog.object :as gobj]
|
|
[goog.dom :as gdom]
|
|
[medley.core :as medley]
|
|
[frontend.ui.date-picker]
|
|
[frontend.context.i18n :as i18n]))
|
|
|
|
(defonce transition-group (r/adapt-class TransitionGroup))
|
|
(defonce css-transition (r/adapt-class CSSTransition))
|
|
(defonce textarea (r/adapt-class (gobj/get TextareaAutosize "default")))
|
|
(def resize-provider (r/adapt-class (gobj/get Resize "ResizeProvider")))
|
|
(def resize-consumer (r/adapt-class (gobj/get Resize "ResizeConsumer")))
|
|
|
|
(rum/defc ls-textarea < rum/reactive
|
|
[{:keys [on-change] :as props}]
|
|
(let [skip-composition? (or
|
|
(state/sub :editor/show-page-search?)
|
|
(state/sub :editor/show-block-search?)
|
|
(state/sub :editor/show-template-search?))
|
|
on-composition (fn [e]
|
|
(if skip-composition?
|
|
(on-change e)
|
|
(case e.type
|
|
"compositionend" (do
|
|
(state/set-editor-in-composition! false)
|
|
(on-change e))
|
|
(state/set-editor-in-composition! true))))
|
|
props (assoc props
|
|
:on-change (fn [e] (when-not (state/editor-in-composition?)
|
|
(on-change e)))
|
|
:on-composition-start on-composition
|
|
:on-composition-update on-composition
|
|
:on-composition-end on-composition)]
|
|
(textarea props)))
|
|
|
|
(rum/defc dropdown-content-wrapper [state content class]
|
|
(let [class (or class
|
|
(util/hiccup->class "origin-top-right.absolute.right-0.mt-2.rounded-md.shadow-lg"))]
|
|
[:div.dropdown-wrapper
|
|
{:class (str class " "
|
|
(case state
|
|
"entering" "transition ease-out duration-100 transform opacity-0 scale-95"
|
|
"entered" "transition ease-out duration-100 transform opacity-100 scale-100"
|
|
"exiting" "transition ease-in duration-75 transform opacity-100 scale-100"
|
|
"exited" "transition ease-in duration-75 transform opacity-0 scale-95"))}
|
|
content]))
|
|
|
|
;; public exports
|
|
(rum/defcs dropdown < (mixins/modal :open?)
|
|
[state content-fn modal-content-fn
|
|
& [{:keys [modal-class z-index]
|
|
:or {z-index 999}
|
|
:as opts}]]
|
|
(let [{:keys [open? toggle-fn]} state
|
|
modal-content (modal-content-fn state)]
|
|
[:div.ml-1.relative {:style {:z-index z-index}}
|
|
(content-fn state)
|
|
(css-transition
|
|
{:in @open? :timeout 0}
|
|
(fn [dropdown-state]
|
|
(when @open?
|
|
(dropdown-content-wrapper dropdown-state modal-content modal-class))))]))
|
|
|
|
(rum/defc menu-link
|
|
[options child]
|
|
[:a.block.px-4.py-2.text-sm.text-gray-700.transition.ease-in-out.duration-150.cursor.menu-link
|
|
options
|
|
child])
|
|
|
|
(rum/defc dropdown-with-links
|
|
[content-fn links {:keys [modal-class links-header links-footer z-index] :as opts}]
|
|
(dropdown
|
|
content-fn
|
|
(fn [{:keys [close-fn] :as state}]
|
|
[:div.py-1.rounded-md.shadow-xs
|
|
(when links-header links-header)
|
|
(for [{:keys [options title icon]} links]
|
|
(let [new-options
|
|
(assoc options
|
|
:on-click (fn [e]
|
|
(when-let [on-click-fn (:on-click options)]
|
|
(on-click-fn e))
|
|
(close-fn)))
|
|
child [:div
|
|
{:style {:display "flex" :flex-direction "row"}}
|
|
[:div {:style {:margin-right "8px"}} title]
|
|
;; [:div {:style {:position "absolute" :right "8px"}}
|
|
;; icon]
|
|
]]
|
|
(rum/with-key
|
|
(menu-link new-options child)
|
|
title)))
|
|
(when links-footer links-footer)])
|
|
opts))
|
|
|
|
(defn button
|
|
[text & {:keys [background href class intent]
|
|
:as option}]
|
|
(let [klass (if-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700")
|
|
klass (if background (string/replace klass "indigo" background) klass)]
|
|
(if href
|
|
[:a.ui__button.is-link
|
|
(merge
|
|
{:type "button"
|
|
:class (str (util/hiccup->class klass) " " class)}
|
|
(dissoc option :background))
|
|
text]
|
|
[:button.ui__button
|
|
(merge
|
|
{:type "button"
|
|
:class (str (util/hiccup->class klass) " " class)}
|
|
(dissoc option :background))
|
|
text])))
|
|
|
|
(rum/defc notification-content
|
|
[state content status uid]
|
|
(when (and content status)
|
|
(let [[color-class svg]
|
|
(case status
|
|
:success
|
|
["text-gray-900 dark:text-gray-300 "
|
|
[:svg.h-6.w-6.text-green-400
|
|
{:stroke "currentColor", :viewBox "0 0 24 24", :fill "none"}
|
|
[:path
|
|
{:d "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
:stroke-width "2"
|
|
:stroke-linejoin "round"
|
|
:stroke-linecap "round"}]]]
|
|
:warning
|
|
["text-gray-900 dark:text-gray-300 "
|
|
[:svg.h-6.w-6.text-yellow-500
|
|
{:stroke "currentColor", :viewBox "0 0 24 24", :fill "none"}
|
|
[:path
|
|
{:d "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
:stroke-width "2"
|
|
:stroke-linejoin "round"
|
|
:stroke-linecap "round"}]]]
|
|
|
|
["text-red-500"
|
|
[:svg.h-6.w-6.text-red-500
|
|
{:view-box "0 0 20 20", :fill "currentColor"}
|
|
[:path
|
|
{:clip-rule "evenodd"
|
|
:d
|
|
"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
|
:fill-rule "evenodd"}]]])]
|
|
[:div.ui__notifications-content
|
|
{:style {:z-index (if (or (= state "exiting")
|
|
(= state "exited"))
|
|
-1
|
|
99)
|
|
:top "3.2em"}}
|
|
[:div.max-w-sm.w-full.shadow-lg.rounded-lg.pointer-events-auto.notification-area
|
|
{:class (case state
|
|
"entering" "transition ease-out duration-300 transform opacity-0 translate-y-2 sm:translate-x-0"
|
|
"entered" "transition ease-out duration-300 transform translate-y-0 opacity-100 sm:translate-x-0"
|
|
"exiting" "transition ease-in duration-100 opacity-100"
|
|
"exited" "transition ease-in duration-100 opacity-0")}
|
|
[:div.rounded-lg.shadow-xs.overflow-hidden
|
|
[:div.p-4
|
|
[:div.flex.items-start
|
|
[:div.flex-shrink-0
|
|
svg]
|
|
[:div.ml-3.w-0.flex-1
|
|
[:div.text-sm.leading-5.font-medium {:style {:margin 0}
|
|
:class color-class}
|
|
content]]
|
|
[:div.ml-4.flex-shrink-0.flex
|
|
[:button.inline-flex.text-gray-400.focus:outline-none.focus:text-gray-500.transition.ease-in-out.duration-150
|
|
{:on-click (fn []
|
|
(notification-handler/clear! uid))}
|
|
[:svg.h-5.w-5
|
|
{:fill "currentColor", :view-Box "0 0 20 20"}
|
|
[:path
|
|
{:clip-rule "evenodd"
|
|
:d
|
|
"M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
:fill-rule "evenodd"}]]]]]]]]])))
|
|
|
|
(rum/defc notification < rum/reactive
|
|
[]
|
|
(let [contents (state/sub :notification/contents)]
|
|
(transition-group
|
|
{:class-name "notifications ui__notifications"}
|
|
(doall (map (fn [el]
|
|
(let [k (first el)
|
|
v (second el)]
|
|
(css-transition
|
|
{:timeout 100
|
|
:key (name k)}
|
|
(fn [state]
|
|
(notification-content state (:content v) (:status v) k)))))
|
|
contents)))))
|
|
|
|
(defn checkbox
|
|
[option]
|
|
[:input.form-checkbox.h-4.w-4.transition.duration-150.ease-in-out
|
|
(merge {:type "checkbox"} option)])
|
|
|
|
(defn badge
|
|
[text option]
|
|
[:span.inline-flex.items-center.px-2.5.py-0.5.rounded-full.text-xs.font-medium.leading-4.bg-purple-100.text-purple-800
|
|
option
|
|
text])
|
|
|
|
;; scroll
|
|
(defn get-doc-scroll-top []
|
|
(.-scrollTop js/document.documentElement))
|
|
|
|
(defn main-node
|
|
[]
|
|
(gdom/getElement "main-content"))
|
|
|
|
(defn get-scroll-top []
|
|
(.-scrollTop (main-node)))
|
|
|
|
(defn get-dynamic-style-node
|
|
[]
|
|
(js/document.getElementById "dynamic-style-scope"))
|
|
|
|
(defn inject-document-devices-envs!
|
|
[]
|
|
(let [cl (.-classList js/document.documentElement)]
|
|
(if util/mac? (.add cl "is-mac"))
|
|
(if util/win32? (.add cl "is-win32"))
|
|
(if (util/electron?) (.add cl "is-electron"))
|
|
(if (util/ios?) (.add cl "is-ios"))
|
|
(if (util/mobile?) (.add cl "is-mobile"))
|
|
(if (util/safari?) (.add cl "is-safari"))
|
|
(when (util/electron?)
|
|
(js/window.apis.on "full-screen" #(js-invoke cl (if (= % "enter") "add" "remove") "is-fullscreen")))))
|
|
|
|
(defn inject-dynamic-style-node!
|
|
[]
|
|
(let [style (get-dynamic-style-node)]
|
|
(if (nil? style)
|
|
(let [node (js/document.createElement "style")]
|
|
(set! (.-id node) "dynamic-style-scope")
|
|
(.appendChild js/document.head node))
|
|
style)))
|
|
|
|
(defn setup-patch-ios-fixed-bottom-position!
|
|
"fix a common issue about ios webpage viewport
|
|
when soft keyboard setup"
|
|
[]
|
|
(if (and
|
|
(util/ios?)
|
|
(not (nil? js/window.visualViewport)))
|
|
(let [viewport js/visualViewport
|
|
style (get-dynamic-style-node)
|
|
sheet (.-sheet style)
|
|
raf-pending? (atom false)
|
|
set-raf-pending! #(reset! raf-pending? %)
|
|
handler
|
|
(fn []
|
|
(if-not @raf-pending?
|
|
(let [f (fn []
|
|
(set-raf-pending! false)
|
|
(let [vh (+ (.-offsetTop viewport) (.-height viewport))
|
|
rule (.. sheet -rules (item 0))
|
|
set-top #(set! (.. rule -style -top) (str % "px"))]
|
|
(set-top vh)))]
|
|
(set-raf-pending! true)
|
|
(js/window.requestAnimationFrame f))))]
|
|
(.insertRule sheet ".fix-ios-fixed-bottom {bottom:unset !important; transform: translateY(-100%); top: 100vh;}")
|
|
(.addEventListener viewport "resize" handler)
|
|
(.addEventListener viewport "scroll" handler)
|
|
(fn []
|
|
(.removeEventListener viewport "resize" handler)
|
|
(.removeEventListener viewport "scroll" handler)))))
|
|
|
|
(defn on-scroll
|
|
[on-load on-top-reached]
|
|
(let [node js/document.documentElement
|
|
full-height (gobj/get node "scrollHeight")
|
|
scroll-top (gobj/get node "scrollTop")
|
|
client-height (gobj/get node "clientHeight")
|
|
bottom-reached? (<= (- full-height scroll-top client-height) 100)
|
|
top-reached? (= scroll-top 0)]
|
|
(when (and bottom-reached? on-load)
|
|
(on-load))
|
|
(when (and top-reached? on-top-reached)
|
|
(on-top-reached))))
|
|
|
|
(defn attach-listeners
|
|
"Attach scroll and resize listeners."
|
|
[state]
|
|
(let [opts (-> state :rum/args second)
|
|
debounced-on-scroll (util/debounce 500 #(on-scroll
|
|
(:on-load opts) ; bottom reached
|
|
(:on-top-reached opts)))]
|
|
(mixins/listen state js/document :scroll debounced-on-scroll)))
|
|
|
|
(rum/defcs infinite-list <
|
|
(mixins/event-mixin attach-listeners)
|
|
"Render an infinite list."
|
|
[state body {:keys [on-load has-more]}]
|
|
(rum/with-context [[t] i18n/*tongue-context*]
|
|
(rum/fragment
|
|
body
|
|
[:a.fade-link.text-link.font-bold.text-4xl
|
|
{:on-click on-load
|
|
:disabled (not has-more)
|
|
:class (when (not has-more) "cursor-not-allowed ")}
|
|
(t (if has-more :page/earlier :page/no-more-journals))])))
|
|
|
|
(rum/defcs auto-complete <
|
|
(rum/local 0 ::current-idx)
|
|
(mixins/event-mixin
|
|
(fn [state]
|
|
(mixins/on-key-down
|
|
state
|
|
{;; up
|
|
38 (fn [_ e]
|
|
(let [current-idx (get state ::current-idx)
|
|
matched (first (:rum/args state))]
|
|
(util/stop e)
|
|
(cond
|
|
(>= @current-idx 1)
|
|
(swap! current-idx dec)
|
|
(= @current-idx 0)
|
|
(reset! current-idx (dec (count matched)))
|
|
|
|
:else
|
|
nil)
|
|
(when-let [element (gdom/getElement (str "ac-" @current-idx))]
|
|
(let [ac-inner (gdom/getElement "ui__ac-inner")
|
|
element-top (gobj/get element "offsetTop")
|
|
scroll-top (- (gobj/get element "offsetTop") 360)]
|
|
(set! (.-scrollTop ac-inner) scroll-top)))))
|
|
;; down
|
|
40 (fn [state e]
|
|
(let [current-idx (get state ::current-idx)
|
|
matched (first (:rum/args state))]
|
|
(util/stop e)
|
|
(let [total (count matched)]
|
|
(if (>= @current-idx (dec total))
|
|
(reset! current-idx 0)
|
|
(swap! current-idx inc)))
|
|
(when-let [element (gdom/getElement (str "ac-" @current-idx))]
|
|
(let [ac-inner (gdom/getElement "ui__ac-inner")
|
|
element-top (gobj/get element "offsetTop")
|
|
scroll-top (- (gobj/get element "offsetTop") 360)]
|
|
(set! (.-scrollTop ac-inner) scroll-top)))))
|
|
|
|
;; enter
|
|
13 (fn [state e]
|
|
(util/stop e)
|
|
(let [[matched {:keys [on-chosen on-enter]}] (:rum/args state)]
|
|
(let [current-idx (get state ::current-idx)]
|
|
(if (and (seq matched)
|
|
(> (count matched)
|
|
@current-idx))
|
|
(on-chosen (nth matched @current-idx) false)
|
|
(and on-enter (on-enter state))))))})))
|
|
[state matched {:keys [on-chosen
|
|
on-shift-chosen
|
|
on-enter
|
|
empty-div
|
|
item-render
|
|
class]}]
|
|
(let [current-idx (get state ::current-idx)]
|
|
[:div#ui__ac {:class class}
|
|
(if (seq matched)
|
|
[:div#ui__ac-inner.hide-scrollbar
|
|
(for [[idx item] (medley/indexed matched)]
|
|
(rum/with-key
|
|
(menu-link
|
|
{:id (str "ac-" idx)
|
|
:class (when (= @current-idx idx)
|
|
"chosen")
|
|
;; :tab-index -1
|
|
:on-click (fn [e]
|
|
(.preventDefault e)
|
|
(if (and (gobj/get e "shiftKey") on-shift-chosen)
|
|
(on-shift-chosen item)
|
|
(on-chosen item)))}
|
|
(if item-render (item-render item) item))
|
|
idx))]
|
|
(when empty-div
|
|
empty-div))]))
|
|
|
|
(def datepicker frontend.ui.date-picker/date-picker)
|
|
|
|
(defn toggle
|
|
([on? on-click] (toggle on? on-click false))
|
|
([on? on-click small?]
|
|
[:a.ui__toggle {:on-click on-click
|
|
:class (if small? "is-small" "")}
|
|
[:span.wrapper.transition-colors.ease-in-out.duration-200
|
|
{:aria-checked "false", :tab-index "0", :role "checkbox"
|
|
:class (if on? "bg-indigo-600" "bg-gray-200")}
|
|
[:span.switcher.transform.transition.ease-in-out.duration-200
|
|
{:class (if on? (if small? "translate-x-4" "translate-x-5") "translate-x-0")
|
|
:aria-hidden "true"}]]]))
|
|
|
|
(defn tooltip
|
|
([label children]
|
|
(tooltip label children {}))
|
|
([label children {:keys [label-style]}]
|
|
[:div.Tooltip {:style {:display "inline"}}
|
|
[:div (cond->
|
|
{:class "Tooltip__label"}
|
|
label-style
|
|
(assoc :style label-style))
|
|
label]
|
|
children]))
|
|
|
|
(defonce modal-show? (atom false))
|
|
(rum/defc modal-overlay
|
|
[state]
|
|
[:div.ui__modal-overlay
|
|
{:class (case state
|
|
"entering" "ease-out duration-300 opacity-0"
|
|
"entered" "ease-out duration-300 opacity-100"
|
|
"exiting" "ease-in duration-200 opacity-100"
|
|
"exited" "ease-in duration-200 opacity-0")}
|
|
[:div.absolute.inset-0.opacity-75]])
|
|
|
|
(rum/defc modal-panel
|
|
[panel-content transition-state close-fn fullscreen?]
|
|
[:div.ui__modal-panel.transform.transition-all.sm:min-w-lg.sm
|
|
{:class (case transition-state
|
|
"entering" "ease-out duration-300 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
"entered" "ease-out duration-300 opacity-100 translate-y-0 sm:scale-100"
|
|
"exiting" "ease-in duration-200 opacity-100 translate-y-0 sm:scale-100"
|
|
"exited" "ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95")}
|
|
[:div.absolute.top-0.right-0.pt-2.pr-2
|
|
[:button.ui__modal-close
|
|
{:aria-label "Close"
|
|
:type "button"
|
|
:on-click (fn [] (apply (or (gobj/get close-fn "user-close") close-fn) []))}
|
|
[:svg.h-6.w-6
|
|
{:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
|
|
[:path
|
|
{:d "M6 18L18 6M6 6l12 12"
|
|
:stroke-width "2"
|
|
:stroke-linejoin "round"
|
|
:stroke-linecap "round"}]]]]
|
|
|
|
[:div {:class (if fullscreen? "" "panel-content")}
|
|
(panel-content close-fn)]])
|
|
|
|
(rum/defc modal < rum/reactive
|
|
(mixins/event-mixin
|
|
(fn [state]
|
|
(mixins/hide-when-esc-or-outside
|
|
state
|
|
:on-hide (fn []
|
|
(-> (.querySelector (rum/dom-node state) "button.ui__modal-close")
|
|
(.click)))
|
|
:outside? false)))
|
|
[]
|
|
(let [modal-panel-content (state/sub :modal/panel-content)
|
|
fullscreen? (state/sub :modal/fullscreen?)
|
|
show? (boolean modal-panel-content)
|
|
close-fn #(state/close-modal!)
|
|
modal-panel-content (or modal-panel-content (fn [close] [:div]))]
|
|
[:div.ui__modal
|
|
{:style {:z-index (if show? 10 -1)}}
|
|
(css-transition
|
|
{:in show? :timeout 0}
|
|
(fn [state]
|
|
(modal-overlay state)))
|
|
(css-transition
|
|
{:in show? :timeout 0}
|
|
(fn [state]
|
|
(modal-panel modal-panel-content state close-fn fullscreen?)))]))
|
|
|
|
(defn make-confirm-modal
|
|
[{:keys [tag title sub-title sub-checkbox? on-cancel on-confirm] :as opts}]
|
|
(fn [close-fn]
|
|
(rum/with-context [[t] i18n/*tongue-context*]
|
|
(let [*sub-checkbox-selected (and sub-checkbox? (atom []))]
|
|
[:div.ui__confirm-modal
|
|
{:class (str "is-" tag)}
|
|
[:div.sm:flex.sm:items-start
|
|
[:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-red-100.sm:mx-0.sm:h-10.sm:w-10
|
|
[:svg.h-6.w-6.text-red-600
|
|
{:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
|
|
[:path
|
|
{:d
|
|
"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
:stroke-width "2"
|
|
:stroke-linejoin "round"
|
|
:stroke-linecap "round"}]]]
|
|
[:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
|
|
[:h2.headline.text-lg.leading-6.font-medium
|
|
(if (keyword? title) (t title) title)]
|
|
[:label.sublabel
|
|
(when sub-checkbox?
|
|
(checkbox
|
|
{:default-value false
|
|
:on-change (fn [e]
|
|
(let [checked (.. e -target -checked)]
|
|
(reset! *sub-checkbox-selected [checked])))}))
|
|
[:h3.subline.text-gray-400
|
|
(if (keyword? sub-title)
|
|
(t sub-title)
|
|
sub-title)]]]]
|
|
|
|
[:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
|
|
[:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
|
|
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
|
|
{:type "button"
|
|
:on-click #(and (fn? on-confirm)
|
|
(on-confirm % {:close-fn close-fn
|
|
:sub-selected (and *sub-checkbox-selected @*sub-checkbox-selected)}))}
|
|
(t :yes)]]
|
|
[:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
|
|
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
|
|
{:type "button"
|
|
:on-click (comp on-cancel close-fn)}
|
|
(t :cancel)]]]]))))
|
|
|
|
(defn loading
|
|
[content]
|
|
[:div.flex.flex-row.items-center
|
|
[:span.icon.flex.items-center svg/loading]
|
|
[:span.text.pl-2 content]])
|
|
|
|
(rum/defcs foldable <
|
|
(rum/local false ::control?)
|
|
(rum/local false ::collapsed?)
|
|
{:will-mount (fn [state]
|
|
(let [args (:rum/args state)]
|
|
(when (true? (last args))
|
|
(reset! (get state ::collapsed?) true)))
|
|
state)}
|
|
[state header content default-collapsed?]
|
|
(let [control? (get state ::control?)
|
|
collapsed? (get state ::collapsed?)]
|
|
[:div.flex.flex-col
|
|
[:div.content
|
|
[:div.flex-1.flex-row.foldable-title {:on-mouse-over #(reset! control? true)
|
|
:on-mouse-out #(reset! control? false)}
|
|
[:div.flex.flex-row.items-center
|
|
[:a.block-control.opacity-50.hover:opacity-100.mr-2
|
|
{:style {:width 14
|
|
:height 16
|
|
:margin-left -24}
|
|
:on-click (fn [e]
|
|
(util/stop e)
|
|
(swap! collapsed? not))}
|
|
(cond
|
|
@collapsed?
|
|
(svg/caret-right)
|
|
|
|
@control?
|
|
(svg/caret-down)
|
|
|
|
:else
|
|
[:span ""])]
|
|
(if (fn? header)
|
|
(header @collapsed?)
|
|
header)]]]
|
|
[:div {:class (if @collapsed?
|
|
"hidden"
|
|
"initial")}
|
|
(cond
|
|
(and (fn? content) (not @collapsed?))
|
|
(content)
|
|
|
|
(fn? content)
|
|
nil
|
|
|
|
:else
|
|
content)]]))
|
|
|
|
(defn admonition
|
|
[type content]
|
|
(let [type (name type)]
|
|
(when-let [icon (case (string/lower-case type)
|
|
"note" svg/note
|
|
"tip" svg/tip
|
|
"important" svg/important
|
|
"caution" svg/caution
|
|
"warning" svg/warning
|
|
"pinned" svg/pinned
|
|
nil)]
|
|
[:div.flex.flex-row.admonitionblock.align-items {:class type}
|
|
[:div.pr-4.admonition-icon.flex.flex-col.justify-center
|
|
{:title (string/upper-case type)} (icon)]
|
|
[:div.ml-4.text-lg
|
|
content]])))
|
|
|
|
(rum/defcs catch-error
|
|
< {:did-catch
|
|
(fn [state error info]
|
|
(js/console.dir error)
|
|
(assoc state ::error error))}
|
|
[{error ::error, c :rum/react-component} error-view view]
|
|
(if (some? error)
|
|
error-view
|
|
view))
|
|
|
|
(rum/defc select
|
|
[options on-change]
|
|
[:select.mt-1.form-select.block.w-full.px-3.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5.ml-4
|
|
{:style {:padding "0 0 0 12px"}
|
|
:on-change (fn [e]
|
|
(let [value (util/evalue e)]
|
|
(on-change value)))}
|
|
(for [{:keys [label value selected]} options]
|
|
[:option (cond->
|
|
{:key label
|
|
:value (or value label)}
|
|
selected
|
|
(assoc :selected selected))
|
|
label])])
|