|  | @@ -0,0 +1,658 @@
 | 
	
		
			
				|  |  | +(ns frontend.extensions.handbooks.core
 | 
	
		
			
				|  |  | +  (:require [clojure.string :as string]
 | 
	
		
			
				|  |  | +            [rum.core :as rum]
 | 
	
		
			
				|  |  | +            [cljs.core.async :as async :refer [<! >!]]
 | 
	
		
			
				|  |  | +            [frontend.ui :as ui]
 | 
	
		
			
				|  |  | +            [frontend.state :as state]
 | 
	
		
			
				|  |  | +            [frontend.search :as search]
 | 
	
		
			
				|  |  | +            [frontend.config :as config]
 | 
	
		
			
				|  |  | +            [frontend.handler.notification :as notification]
 | 
	
		
			
				|  |  | +            [frontend.extensions.lightbox :as lightbox]
 | 
	
		
			
				|  |  | +            [frontend.modules.shortcut.config :as shortcut-config]
 | 
	
		
			
				|  |  | +            [frontend.rum :as r]
 | 
	
		
			
				|  |  | +            [cljs-bean.core :as bean]
 | 
	
		
			
				|  |  | +            [promesa.core :as p]
 | 
	
		
			
				|  |  | +            [camel-snake-kebab.core :as csk]
 | 
	
		
			
				|  |  | +            [medley.core :as medley]
 | 
	
		
			
				|  |  | +            [frontend.util :as util]
 | 
	
		
			
				|  |  | +            [frontend.storage :as storage]
 | 
	
		
			
				|  |  | +            [frontend.extensions.video.youtube :as youtube]
 | 
	
		
			
				|  |  | +            [frontend.context.i18n :refer [t]]
 | 
	
		
			
				|  |  | +            [clojure.edn :as edn]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(defonce *config (atom {}))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(defn get-handbooks-endpoint
 | 
	
		
			
				|  |  | +  [resource]
 | 
	
		
			
				|  |  | +  (str
 | 
	
		
			
				|  |  | +    (if (storage/get :handbooks-dev-watch?)
 | 
	
		
			
				|  |  | +      "http://localhost:1337"
 | 
	
		
			
				|  |  | +      "https://handbooks.pages.dev")
 | 
	
		
			
				|  |  | +    resource))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(defn resolve-asset-url
 | 
	
		
			
				|  |  | +  [path]
 | 
	
		
			
				|  |  | +  (if (string/starts-with? path "http")
 | 
	
		
			
				|  |  | +    path (str (get-handbooks-endpoint "/")
 | 
	
		
			
				|  |  | +              (-> path (string/replace-first "./" "")
 | 
	
		
			
				|  |  | +                  (string/replace-first #"^/+" "")))))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(defn inflate-content-assets-urls
 | 
	
		
			
				|  |  | +  [content]
 | 
	
		
			
				|  |  | +  (if-let [matches (and (not (string/blank? content))
 | 
	
		
			
				|  |  | +                        (re-seq #"src=\"([^\"]+)\"" content))]
 | 
	
		
			
				|  |  | +    (reduce
 | 
	
		
			
				|  |  | +      (fn [content matched]
 | 
	
		
			
				|  |  | +        (if-let [matched (second matched)]
 | 
	
		
			
				|  |  | +          (string/replace content matched (resolve-asset-url matched)) content))
 | 
	
		
			
				|  |  | +      content matches)
 | 
	
		
			
				|  |  | +    content))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(defn parse-key-from-href
 | 
	
		
			
				|  |  | +  [href base]
 | 
	
		
			
				|  |  | +  (when (and (string? href)
 | 
	
		
			
				|  |  | +             (not (string/blank? href)))
 | 
	
		
			
				|  |  | +    (when-let [href (some-> href (string/trim) (string/replace #".edn$" ""))]
 | 
	
		
			
				|  |  | +      (some-> (if (string/starts-with? href "@")
 | 
	
		
			
				|  |  | +                (string/replace href #"^[@\/]+" "")
 | 
	
		
			
				|  |  | +                (util/node-path.join base href))
 | 
	
		
			
				|  |  | +              (string/lower-case)
 | 
	
		
			
				|  |  | +              (csk/->snake_case_string)))))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(defn parse-parent-key
 | 
	
		
			
				|  |  | +  [s]
 | 
	
		
			
				|  |  | +  (if (and (string? s) (string/includes? s "/"))
 | 
	
		
			
				|  |  | +    (subs s 0 (string/last-index-of s "/"))
 | 
	
		
			
				|  |  | +    s))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(defn bind-parent-key
 | 
	
		
			
				|  |  | +  [{:keys [key] :as node}]
 | 
	
		
			
				|  |  | +  (cond-> node
 | 
	
		
			
				|  |  | +          (and (string? key)
 | 
	
		
			
				|  |  | +               (string/includes? key "/"))
 | 
	
		
			
				|  |  | +          (assoc :parent (parse-parent-key key))))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(defn load-glide-assets!
 | 
	
		
			
				|  |  | +  []
 | 
	
		
			
				|  |  | +  (p/let [_ (util/css-load$ (str util/JS_ROOT "/glide/glide.core.min.css"))
 | 
	
		
			
				|  |  | +          _ (util/css-load$ (str util/JS_ROOT "/glide/glide.theme.min.css"))
 | 
	
		
			
				|  |  | +          _ (when-not (aget js/window "Glide")
 | 
	
		
			
				|  |  | +              (util/js-load$ (str util/JS_ROOT "/glide/glide.min.js")))]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc topic-card
 | 
	
		
			
				|  |  | +  [{:keys [key title description cover] :as _topic} nav-fn! opts]
 | 
	
		
			
				|  |  | +  [:button.w-full.topic-card.flex.text-left
 | 
	
		
			
				|  |  | +   (merge
 | 
	
		
			
				|  |  | +     {:key      key
 | 
	
		
			
				|  |  | +      :on-click nav-fn!} opts)
 | 
	
		
			
				|  |  | +   (when cover
 | 
	
		
			
				|  |  | +     [:div.l.flex.items-center
 | 
	
		
			
				|  |  | +      [:img {:src (resolve-asset-url cover)}]])
 | 
	
		
			
				|  |  | +   [:div.r.flex.flex-col
 | 
	
		
			
				|  |  | +    [:strong title]
 | 
	
		
			
				|  |  | +    [:span description]]])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc pane-category-topics
 | 
	
		
			
				|  |  | +  [handbook-nodes pane-state nav!]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  [:div.pane.pane-category-topics
 | 
	
		
			
				|  |  | +   [:div.topics-list
 | 
	
		
			
				|  |  | +    (let [category-key (:key (second pane-state))]
 | 
	
		
			
				|  |  | +      (when-let [category (get handbook-nodes category-key)]
 | 
	
		
			
				|  |  | +        (for [topic (:children category)]
 | 
	
		
			
				|  |  | +          (rum/with-key
 | 
	
		
			
				|  |  | +            (topic-card topic #(nav! [:topic-detail topic (:title category)] pane-state) nil)
 | 
	
		
			
				|  |  | +            (:key topic)))))]])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc media-render
 | 
	
		
			
				|  |  | +  [src]
 | 
	
		
			
				|  |  | +  (let [src (util/trim-safe src)
 | 
	
		
			
				|  |  | +        extname (some-> src (util/full-path-extname) (subs 1))
 | 
	
		
			
				|  |  | +        youtube-id (and (string/includes? src "youtube.com/watch?v=")
 | 
	
		
			
				|  |  | +                        (subs src (+ 2 (string/last-index-of src "v="))))]
 | 
	
		
			
				|  |  | +    (cond
 | 
	
		
			
				|  |  | +      (and extname (contains? config/video-formats (keyword extname)))
 | 
	
		
			
				|  |  | +      [:video {:src src :controls true}]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      (string? youtube-id)
 | 
	
		
			
				|  |  | +      (youtube/youtube-video youtube-id {:width "100%" :height 235})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      :else [:img {:src src}])))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc chapter-select
 | 
	
		
			
				|  |  | +  [topic children on-select]
 | 
	
		
			
				|  |  | +  (let [[open?, set-open?] (rum/use-state false)]
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      (fn []
 | 
	
		
			
				|  |  | +        (when-let [^js el (js/document.querySelector "[data-identity=logseq-handbooks]")]
 | 
	
		
			
				|  |  | +          (let [h #(when-not (some->> (.-target %)
 | 
	
		
			
				|  |  | +                                      (.contains (js/document.querySelector ".chapters-select")))
 | 
	
		
			
				|  |  | +                     (set-open? false))]
 | 
	
		
			
				|  |  | +            (.addEventListener el "click" h)
 | 
	
		
			
				|  |  | +            #(.removeEventListener el "click" h))))
 | 
	
		
			
				|  |  | +      [])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    [:div.chapters-select.w-full
 | 
	
		
			
				|  |  | +     [:a.select-trigger
 | 
	
		
			
				|  |  | +      {:on-click #(set-open? (not open?))
 | 
	
		
			
				|  |  | +       :tabIndex "0"}
 | 
	
		
			
				|  |  | +      [:small "Current chapter"]
 | 
	
		
			
				|  |  | +      [:strong (:title topic)]
 | 
	
		
			
				|  |  | +      (if open?
 | 
	
		
			
				|  |  | +        (ui/icon "chevron-down")
 | 
	
		
			
				|  |  | +        (ui/icon "chevron-left"))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      (when open?
 | 
	
		
			
				|  |  | +        [:ul
 | 
	
		
			
				|  |  | +         (for [c children]
 | 
	
		
			
				|  |  | +           (when (and (seq c) (not= (:key c) (:key topic)))
 | 
	
		
			
				|  |  | +             [:li {:key (:key c)}
 | 
	
		
			
				|  |  | +              [:a.flex {:tabIndex "0" :on-click #(on-select (:key c))}
 | 
	
		
			
				|  |  | +               (or (:title c) (:key c))]]))])]]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc ^:large-vars/cleanup-todo pane-topic-detail
 | 
	
		
			
				|  |  | +  [handbook-nodes pane-state nav!]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  (let [[deps-pending?, set-deps-pending?] (rum/use-state false)
 | 
	
		
			
				|  |  | +        *id-ref (rum/use-ref (str "glide--" (js/Date.now)))]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    ;; load deps assets
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      (fn []
 | 
	
		
			
				|  |  | +        (set-deps-pending? true)
 | 
	
		
			
				|  |  | +        (-> (load-glide-assets!)
 | 
	
		
			
				|  |  | +            (p/then (fn [] (js/setTimeout
 | 
	
		
			
				|  |  | +                             #(when (js/document.getElementById (rum/deref *id-ref))
 | 
	
		
			
				|  |  | +                                (doto (js/window.Glide. (str "#" (rum/deref *id-ref))) (.mount))) 50)))
 | 
	
		
			
				|  |  | +            (p/finally #(set-deps-pending? false))))
 | 
	
		
			
				|  |  | +      [])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      (fn []
 | 
	
		
			
				|  |  | +        (js/setTimeout #(some-> (js/document.querySelector ".cp__handbooks-content")
 | 
	
		
			
				|  |  | +                                (.scrollTo 0 0))))
 | 
	
		
			
				|  |  | +      [pane-state])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    (when-let [topic-key (:key (second pane-state))]
 | 
	
		
			
				|  |  | +      (when-let [topic (get handbook-nodes topic-key)]
 | 
	
		
			
				|  |  | +        (let [chapters (:children topic)
 | 
	
		
			
				|  |  | +              has-chapters? (seq chapters)
 | 
	
		
			
				|  |  | +              topic (if has-chapters? (first chapters) topic)
 | 
	
		
			
				|  |  | +              parent (get handbook-nodes (:parent (bind-parent-key topic)))
 | 
	
		
			
				|  |  | +              chapters (or chapters (:children parent))
 | 
	
		
			
				|  |  | +              parent-key (:key parent)
 | 
	
		
			
				|  |  | +              parent-category? (not (string/includes? parent-key "/"))
 | 
	
		
			
				|  |  | +              show-chapters? (and (not parent-category?) (seq chapters))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +              chapters-len (count chapters)
 | 
	
		
			
				|  |  | +              chapter-current-idx (when-not (zero? chapters-len)
 | 
	
		
			
				|  |  | +                                    (util/find-index #(= (:key %) (:key topic)) chapters))]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          (when-not deps-pending?
 | 
	
		
			
				|  |  | +            [:div.pane.pane-topic-detail
 | 
	
		
			
				|  |  | +             (when-not show-chapters?
 | 
	
		
			
				|  |  | +               [:h1.text-2xl.pb-3.font-semibold (:title topic)])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +             ;; chapters list
 | 
	
		
			
				|  |  | +             (when show-chapters?
 | 
	
		
			
				|  |  | +               [:div.chapters-wrap.py-2
 | 
	
		
			
				|  |  | +                (chapter-select
 | 
	
		
			
				|  |  | +                  topic chapters
 | 
	
		
			
				|  |  | +                  (fn [k]
 | 
	
		
			
				|  |  | +                    (when-let [chapter (get handbook-nodes k)]
 | 
	
		
			
				|  |  | +                      (nav! [:topic-detail chapter (:title parent)] pane-state))))])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +             ;; demos gallery
 | 
	
		
			
				|  |  | +             (when-let [demos (:demos topic)]
 | 
	
		
			
				|  |  | +               (let [demos (cond-> demos
 | 
	
		
			
				|  |  | +                                   (string? demos) (list))]
 | 
	
		
			
				|  |  | +                 (if (> (count demos) 1)
 | 
	
		
			
				|  |  | +                   [:div.flex.demos.glide
 | 
	
		
			
				|  |  | +                    {:id (rum/deref *id-ref)}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    [:div.glide__track {:data-glide-el "track"}
 | 
	
		
			
				|  |  | +                     [:div.glide__slides
 | 
	
		
			
				|  |  | +                      (for [demo demos]
 | 
	
		
			
				|  |  | +                        [:div.item.glide__slide
 | 
	
		
			
				|  |  | +                         (media-render (resolve-asset-url demo))])]]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    [:div.glide__bullets {:data-glide-el "controls[nav]"}
 | 
	
		
			
				|  |  | +                     (map-indexed
 | 
	
		
			
				|  |  | +                       (fn [idx _]
 | 
	
		
			
				|  |  | +                         [:button.glide__bullet {:data-glide-dir (str "=" idx)}
 | 
	
		
			
				|  |  | +                          (inc idx)])
 | 
	
		
			
				|  |  | +                       demos)]]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                   [:div.flex.demos.pt-1
 | 
	
		
			
				|  |  | +                    (media-render (resolve-asset-url (first demos)))])))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +             [:div.content-wrap
 | 
	
		
			
				|  |  | +              (when-let [content (:content topic)]
 | 
	
		
			
				|  |  | +                [:<>
 | 
	
		
			
				|  |  | +                 [:div.content.markdown-body
 | 
	
		
			
				|  |  | +                  {:dangerouslySetInnerHTML {:__html (inflate-content-assets-urls content)}
 | 
	
		
			
				|  |  | +                   :on-click                (fn [^js e]
 | 
	
		
			
				|  |  | +                                              (when-let [target (.-target e)]
 | 
	
		
			
				|  |  | +                                                (if-let [^js img (.closest target "img")]
 | 
	
		
			
				|  |  | +                                                  (lightbox/preview-images! [{:src (.-src img)
 | 
	
		
			
				|  |  | +                                                                              :w   (.-naturalWidth img)
 | 
	
		
			
				|  |  | +                                                                              :h   (.-naturalHeight img)}])
 | 
	
		
			
				|  |  | +                                                  (when-let [link (some-> (.closest target "a") (.getAttribute "href"))]
 | 
	
		
			
				|  |  | +                                                    (when-let [to-k (and (not (string/starts-with? link "http"))
 | 
	
		
			
				|  |  | +                                                                         (parse-key-from-href link parent-key))]
 | 
	
		
			
				|  |  | +                                                      (if-let [to (get handbook-nodes to-k)]
 | 
	
		
			
				|  |  | +                                                        (nav! [:topic-detail to (:title parent)] pane-state)
 | 
	
		
			
				|  |  | +                                                        (js/console.error "ERROR: handbook link resource not found: " to-k link))
 | 
	
		
			
				|  |  | +                                                      (util/stop e))))))}]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                 (when-let [idx (and (> chapters-len 1) chapter-current-idx)]
 | 
	
		
			
				|  |  | +                   (let [prev (when-not (zero? idx) (dec idx))
 | 
	
		
			
				|  |  | +                         next (when-not (= idx (dec chapters-len)) (inc idx))]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                     [:div.controls.flex.justify-between.pt-4
 | 
	
		
			
				|  |  | +                      [:div (when prev (ui/button [:span.flex.items-center (ui/icon "arrow-left") "Prev chapter"]
 | 
	
		
			
				|  |  | +                                                  :small? true :on-click #(nav! [:topic-detail (nth chapters prev) (:title parent)] pane-state)))]
 | 
	
		
			
				|  |  | +                      [:div (when next (ui/button [:span.flex.items-center "Next chapter" (ui/icon "arrow-right")]
 | 
	
		
			
				|  |  | +                                                  :small? true :on-click #(nav! [:topic-detail (nth chapters next) (:title parent)] pane-state)))]]))])]]))))))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc pane-dashboard
 | 
	
		
			
				|  |  | +  [handbooks-nodes pane-state nav-to-pane!]
 | 
	
		
			
				|  |  | +  (when-let [root (get handbooks-nodes "__root")]
 | 
	
		
			
				|  |  | +    [:div.pane.dashboard-pane
 | 
	
		
			
				|  |  | +     (when-let [popular-topics (:popular-topics root)]
 | 
	
		
			
				|  |  | +       [:<>
 | 
	
		
			
				|  |  | +        [:h2 (t :handbook/popular-topics)]
 | 
	
		
			
				|  |  | +        [:div.topics-list
 | 
	
		
			
				|  |  | +         (for [topic-key popular-topics]
 | 
	
		
			
				|  |  | +           (when-let [topic (and (string? topic-key)
 | 
	
		
			
				|  |  | +                                 (->> (util/safe-lower-case topic-key)
 | 
	
		
			
				|  |  | +                                      (csk/->snake_case_string)
 | 
	
		
			
				|  |  | +                                      (get handbooks-nodes)))]
 | 
	
		
			
				|  |  | +             (topic-card topic #(nav-to-pane! [:topic-detail topic (t :handbook/title)] [:dashboard]) nil)))]])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +     [:h2 (t :handbook/help-categories)]
 | 
	
		
			
				|  |  | +     [:div.categories-list
 | 
	
		
			
				|  |  | +      (let [categories (:children root)
 | 
	
		
			
				|  |  | +            categories (conj (vec categories)
 | 
	
		
			
				|  |  | +                             {:key      :ls-shortcuts
 | 
	
		
			
				|  |  | +                              :title    [:span "Keyboard shortcuts"]
 | 
	
		
			
				|  |  | +                              :children [:span (->> (vals @shortcut-config/*config)
 | 
	
		
			
				|  |  | +                                                    (map count)
 | 
	
		
			
				|  |  | +                                                    (apply +))
 | 
	
		
			
				|  |  | +                                         " shortcuts"]
 | 
	
		
			
				|  |  | +                              :color    "#2563EB"
 | 
	
		
			
				|  |  | +                              :icon     "command"})]
 | 
	
		
			
				|  |  | +        (for [{:keys [key title children color icon] :as category} categories
 | 
	
		
			
				|  |  | +              :let [total (if counted? (count children) 0)]]
 | 
	
		
			
				|  |  | +          [:button.category-card.text-left
 | 
	
		
			
				|  |  | +           {:key      key
 | 
	
		
			
				|  |  | +            :style    {:border-left-color (or (ui/->block-background-color color) "var(--ls-secondary-background-color)")}
 | 
	
		
			
				|  |  | +            :data-total total
 | 
	
		
			
				|  |  | +            :on-click #(if (= key :ls-shortcuts)
 | 
	
		
			
				|  |  | +                         (do (state/toggle! :ui/handbooks-open?)
 | 
	
		
			
				|  |  | +                             (state/open-right-sidebar!)
 | 
	
		
			
				|  |  | +                             (state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings))
 | 
	
		
			
				|  |  | +                         (nav-to-pane! [:topics category title] pane-state))}
 | 
	
		
			
				|  |  | +           [:div.icon-wrap
 | 
	
		
			
				|  |  | +            (ui/icon (or icon "chart-bubble") {:size 20})]
 | 
	
		
			
				|  |  | +           [:div.text-wrap
 | 
	
		
			
				|  |  | +            [:strong title]
 | 
	
		
			
				|  |  | +            (cond
 | 
	
		
			
				|  |  | +              (vector? children)
 | 
	
		
			
				|  |  | +              children
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +              :else
 | 
	
		
			
				|  |  | +              [:span (str total " " (util/safe-lower-case (t :handbook/topics)))])]]))]]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc pane-settings
 | 
	
		
			
				|  |  | +  [dev-watch? set-dev-watch?]
 | 
	
		
			
				|  |  | +  [:div.pane.pane-settings
 | 
	
		
			
				|  |  | +   [:div.item
 | 
	
		
			
				|  |  | +    [:p.flex.items-center.space-x-3.mb-0
 | 
	
		
			
				|  |  | +     [:strong "Writing mode (preview in time)"]
 | 
	
		
			
				|  |  | +     (ui/toggle dev-watch? #(set-dev-watch? (not dev-watch?)) true)]
 | 
	
		
			
				|  |  | +    [:small.opacity-30 (str "Resources from " (get-handbooks-endpoint "/"))]]])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc search-bar
 | 
	
		
			
				|  |  | +  [pane-state nav! handbooks-nodes search-state set-search-state!]
 | 
	
		
			
				|  |  | +  (let [*input-ref (rum/use-ref nil)
 | 
	
		
			
				|  |  | +        [q, set-q!] (rum/use-state "")
 | 
	
		
			
				|  |  | +        [results, set-results!] (rum/use-state nil)
 | 
	
		
			
				|  |  | +        [selected, set-selected!] (rum/use-state 0)
 | 
	
		
			
				|  |  | +        select-fn! #(when-let [ldx (and (seq results) (dec (count results)))]
 | 
	
		
			
				|  |  | +                      (set-selected!
 | 
	
		
			
				|  |  | +                        (case %
 | 
	
		
			
				|  |  | +                          :up (if (zero? selected) ldx (max (dec selected) 0))
 | 
	
		
			
				|  |  | +                          :down (if (= selected ldx) 0 (min (inc selected) ldx))
 | 
	
		
			
				|  |  | +                          :dune)))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        q (util/trim-safe q)
 | 
	
		
			
				|  |  | +        active? (not (string/blank? (util/trim-safe q)))
 | 
	
		
			
				|  |  | +        reset-q! #(->> "" (set! (.-value (rum/deref *input-ref))) (set-q!))
 | 
	
		
			
				|  |  | +        focus-q! #(some-> (rum/deref *input-ref) (.focus))]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      #(focus-q!)
 | 
	
		
			
				|  |  | +      [pane-state])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      (fn []
 | 
	
		
			
				|  |  | +        (let [pane-nodes (:children (second pane-state))
 | 
	
		
			
				|  |  | +              pane-nodes (and (seq pane-nodes)
 | 
	
		
			
				|  |  | +                              (mapcat #(conj (:children %) %) pane-nodes))]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          (set-search-state!
 | 
	
		
			
				|  |  | +            (merge search-state {:active? active?}))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          (if (and (seq handbooks-nodes) active?)
 | 
	
		
			
				|  |  | +            (-> (or pane-nodes
 | 
	
		
			
				|  |  | +                    ;; global
 | 
	
		
			
				|  |  | +                    (vals (dissoc handbooks-nodes "__root")))
 | 
	
		
			
				|  |  | +                (search/fuzzy-search q :limit 30 :extract-fn :title)
 | 
	
		
			
				|  |  | +                (set-results!))
 | 
	
		
			
				|  |  | +            (set-results! nil))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          (set-selected! 0)))
 | 
	
		
			
				|  |  | +      [q])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    [:div.search
 | 
	
		
			
				|  |  | +     [:div.input-wrap.relative
 | 
	
		
			
				|  |  | +      [:span.icon.absolute.opacity-90
 | 
	
		
			
				|  |  | +       {:style {:top 6 :left 7}}
 | 
	
		
			
				|  |  | +       (ui/icon "search" {:size 12})]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      [:input {:placeholder   (t :handbook/search)
 | 
	
		
			
				|  |  | +               :auto-focus    true
 | 
	
		
			
				|  |  | +               :default-value q
 | 
	
		
			
				|  |  | +               :on-change     #(set-q! (util/evalue %))
 | 
	
		
			
				|  |  | +               :on-key-down   #(case (.-keyCode %)
 | 
	
		
			
				|  |  | +                                 ;; ESC
 | 
	
		
			
				|  |  | +                                 27
 | 
	
		
			
				|  |  | +                                 (if-not active?
 | 
	
		
			
				|  |  | +                                   (state/toggle! :ui/handbooks-open?)
 | 
	
		
			
				|  |  | +                                   (reset-q!))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                                 ;; Up
 | 
	
		
			
				|  |  | +                                 38
 | 
	
		
			
				|  |  | +                                 (do
 | 
	
		
			
				|  |  | +                                   (util/stop %)
 | 
	
		
			
				|  |  | +                                   (select-fn! :up))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                                 ;; Down
 | 
	
		
			
				|  |  | +                                 40
 | 
	
		
			
				|  |  | +                                 (do
 | 
	
		
			
				|  |  | +                                   (util/stop %)
 | 
	
		
			
				|  |  | +                                   (select-fn! :down))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                                 ;; Enter
 | 
	
		
			
				|  |  | +                                 13
 | 
	
		
			
				|  |  | +                                 (when-let [topic (and active? (nth results selected))]
 | 
	
		
			
				|  |  | +                                   (util/stop %)
 | 
	
		
			
				|  |  | +                                   (nav! [:topic-detail topic (:title topic)] pane-state))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                                 :dune)
 | 
	
		
			
				|  |  | +               :ref           *input-ref}]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      (when active?
 | 
	
		
			
				|  |  | +        [:button.icon.absolute.opacity-50.hover:opacity-80.select-none
 | 
	
		
			
				|  |  | +         {:style    {:right 6 :top 7}
 | 
	
		
			
				|  |  | +          :on-click #(do (reset-q!) (focus-q!))}
 | 
	
		
			
				|  |  | +         (ui/icon "x" {:size 12})])]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +     (when (:active? search-state)
 | 
	
		
			
				|  |  | +       [:div.search-results-wrap
 | 
	
		
			
				|  |  | +        [:div.results-wrap
 | 
	
		
			
				|  |  | +         (for [[idx topic] (medley/indexed results)]
 | 
	
		
			
				|  |  | +           (rum/with-key
 | 
	
		
			
				|  |  | +             (topic-card topic #(nav! [:topic-detail topic (:title topic)] pane-state)
 | 
	
		
			
				|  |  | +                         {:class (util/classnames [{:active (= selected idx)}])})
 | 
	
		
			
				|  |  | +             (:key topic)))]])]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc link-card
 | 
	
		
			
				|  |  | +  [opts child]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  (let [{:keys [href]} opts]
 | 
	
		
			
				|  |  | +    [:div.link-card
 | 
	
		
			
				|  |  | +     (cond-> opts
 | 
	
		
			
				|  |  | +             (string? href)
 | 
	
		
			
				|  |  | +             (assoc :on-click #(util/open-url href)))
 | 
	
		
			
				|  |  | +     child]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +;(rum/defc related-topics
 | 
	
		
			
				|  |  | +;  []
 | 
	
		
			
				|  |  | +;  [:div.related-topics
 | 
	
		
			
				|  |  | +;   (link-card {} [:strong.text-md "How to do something?"])])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(def panes-mapping
 | 
	
		
			
				|  |  | +  {:dashboard    [pane-dashboard]
 | 
	
		
			
				|  |  | +   :topics       [pane-category-topics]
 | 
	
		
			
				|  |  | +   :topic-detail [pane-topic-detail]
 | 
	
		
			
				|  |  | +   :settings     [pane-settings]})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(defonce discord-endpoint "https://plugins.logseq.io/ds")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc footer-link-cards
 | 
	
		
			
				|  |  | +  []
 | 
	
		
			
				|  |  | +  (let [[config _] (r/use-atom *config)
 | 
	
		
			
				|  |  | +        discord-count (:discord-online config)]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      (fn []
 | 
	
		
			
				|  |  | +        (when (or (nil? discord-count)
 | 
	
		
			
				|  |  | +                  (> (- (js/Date.now) (:discord-online-created config)) (* 10 60 1000)))
 | 
	
		
			
				|  |  | +          (-> (js/window.fetch discord-endpoint)
 | 
	
		
			
				|  |  | +              (p/then #(.json %))
 | 
	
		
			
				|  |  | +              (p/then #(when-let [count (.-approximate_presence_count ^js %)]
 | 
	
		
			
				|  |  | +                         (swap! *config assoc
 | 
	
		
			
				|  |  | +                                :discord-online (.toLocaleString count)
 | 
	
		
			
				|  |  | +                                :discord-online-created (js/Date.now)))))))
 | 
	
		
			
				|  |  | +      [discord-count])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    [:<>
 | 
	
		
			
				|  |  | +     ;; more links
 | 
	
		
			
				|  |  | +     [:div.flex.space-x-3
 | 
	
		
			
				|  |  | +      {:style {:padding-top "4px"}}
 | 
	
		
			
				|  |  | +      (link-card
 | 
	
		
			
				|  |  | +        {:class "flex-1" :href "https://discord.gg/KpN4eHY"}
 | 
	
		
			
				|  |  | +        [:div.inner.flex.space-x-1.flex-col
 | 
	
		
			
				|  |  | +         (ui/icon "brand-discord" {:class "opacity-30" :size 26})
 | 
	
		
			
				|  |  | +         [:h1.font-medium.py-1 "Chat on Discord"]
 | 
	
		
			
				|  |  | +         [:h2.text-xs.leading-4.opacity-40 "Ask quick questions, meet fellow users, and learn new workflows."]
 | 
	
		
			
				|  |  | +         [:small.flex.items-center.pt-1.5
 | 
	
		
			
				|  |  | +          [:i.block.rounded-full.bg-green-500 {:style {:width "8px" :height "8px"}}]
 | 
	
		
			
				|  |  | +          [:span.pl-2.opacity-90
 | 
	
		
			
				|  |  | +           [:strong.opacity-60 (or discord-count "?")]
 | 
	
		
			
				|  |  | +           [:span.opacity-70.font-light " users online"]]]])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      (link-card
 | 
	
		
			
				|  |  | +        {:class "flex-1" :href "https://discuss.logseq.com"}
 | 
	
		
			
				|  |  | +        [:div.inner.flex.space-x-1.flex-col
 | 
	
		
			
				|  |  | +         (ui/icon "message-dots" {:class "opacity-30" :size 26})
 | 
	
		
			
				|  |  | +         [:h1.font-medium.py-1 "Visit the forum"]
 | 
	
		
			
				|  |  | +         [:h2.text-xs.leading-4.opacity-40 "Give feedback, request features, and have in-depth conversations."]
 | 
	
		
			
				|  |  | +         [:small.flex.items-center.pt-1.5
 | 
	
		
			
				|  |  | +          [:i.flex.items-center.opacity-50 (ui/icon "bolt" {:size 14})]
 | 
	
		
			
				|  |  | +          [:span.pl-1.opacity-90
 | 
	
		
			
				|  |  | +           [:strong.opacity-60 "800+"]
 | 
	
		
			
				|  |  | +           [:span.opacity-70.font-light " monthly posts"]]]])]]))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +(rum/defc ^:large-vars/data-var content
 | 
	
		
			
				|  |  | +  []
 | 
	
		
			
				|  |  | +  (let [[active-pane-state, set-active-pane-state!]
 | 
	
		
			
				|  |  | +        (rum/use-state [:dashboard nil (t :handbook/title)])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        [handbooks-state, set-handbooks-state!]
 | 
	
		
			
				|  |  | +        (rum/use-state nil)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        [handbooks-nodes, set-handbooks-nodes!]
 | 
	
		
			
				|  |  | +        (rum/use-state nil)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        [history-state, set-history-state!]
 | 
	
		
			
				|  |  | +        (rum/use-state ())
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        [dev-watch?, set-dev-watch?]
 | 
	
		
			
				|  |  | +        (rum/use-state (storage/get :handbooks-dev-watch?))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        [search-state, set-search-state!]
 | 
	
		
			
				|  |  | +        (rum/use-state {:active? false})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        reset-handbooks! #(set-handbooks-state! {:status nil :data nil :error nil})
 | 
	
		
			
				|  |  | +        update-handbooks! #(set-handbooks-state! (fn [v] (merge v %)))
 | 
	
		
			
				|  |  | +        load-handbooks! (fn []
 | 
	
		
			
				|  |  | +                          (when-not (= :pending (:status handbooks-state))
 | 
	
		
			
				|  |  | +                            (reset-handbooks!)
 | 
	
		
			
				|  |  | +                            (update-handbooks! {:status :pending})
 | 
	
		
			
				|  |  | +                            (-> (p/let [^js res (js/fetch (get-handbooks-endpoint "/handbooks.edn"))
 | 
	
		
			
				|  |  | +                                        data (.text res)]
 | 
	
		
			
				|  |  | +                                  (update-handbooks! {:data (edn/read-string data)}))
 | 
	
		
			
				|  |  | +                                (p/catch #(update-handbooks! {:error (str %)}))
 | 
	
		
			
				|  |  | +                                (p/finally #(update-handbooks! {:status :completed})))))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        active-pane-name (first active-pane-state)
 | 
	
		
			
				|  |  | +        pane-render (first (get panes-mapping active-pane-name))
 | 
	
		
			
				|  |  | +        pane-dashboard? (= :dashboard active-pane-name)
 | 
	
		
			
				|  |  | +        pane-settings? (= :settings active-pane-name)
 | 
	
		
			
				|  |  | +        pane-topic? (= :topic-detail active-pane-name)
 | 
	
		
			
				|  |  | +        force-nav-dashboard! (fn []
 | 
	
		
			
				|  |  | +                               (set-active-pane-state! [:dashboard])
 | 
	
		
			
				|  |  | +                               (set-history-state! '()))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        handbooks-loaded? (and (seq (:data handbooks-state))
 | 
	
		
			
				|  |  | +                               (= :completed (:status handbooks-state)))
 | 
	
		
			
				|  |  | +        handbooks-data (:data handbooks-state)
 | 
	
		
			
				|  |  | +        nav-to-pane! (fn [next-state prev-state]
 | 
	
		
			
				|  |  | +                       (let [next-key (:key (second next-state))
 | 
	
		
			
				|  |  | +                             prev-key (:key (second prev-state))
 | 
	
		
			
				|  |  | +                             in-chapters? (and prev-key next-key (string/includes? prev-key "/")
 | 
	
		
			
				|  |  | +                                               (or (string/starts-with? next-key prev-key)
 | 
	
		
			
				|  |  | +                                                   (apply = (map parse-parent-key [prev-key next-key]))))]
 | 
	
		
			
				|  |  | +                         (when-not in-chapters?
 | 
	
		
			
				|  |  | +                           (set-history-state!
 | 
	
		
			
				|  |  | +                             (conj (sequence history-state) prev-state))))
 | 
	
		
			
				|  |  | +                       (set-active-pane-state! next-state))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        [scrolled?, set-scrolled!] (rum/use-state false)
 | 
	
		
			
				|  |  | +        on-scroll (rum/use-memo #(util/debounce 100 (fn [^js e] (set-scrolled! (not (< (.. e -target -scrollTop) 10))))) [])]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    ;; load handbooks
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      #(load-handbooks!)
 | 
	
		
			
				|  |  | +      [])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    ;; navigation sentry
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      (fn []
 | 
	
		
			
				|  |  | +        (when (seq handbooks-nodes)
 | 
	
		
			
				|  |  | +          (let [c (:handbook/route-chan @state/state)]
 | 
	
		
			
				|  |  | +            (async/go-loop []
 | 
	
		
			
				|  |  | +                           (let [v (<! c)]
 | 
	
		
			
				|  |  | +                             (when (not= v :return)
 | 
	
		
			
				|  |  | +                               (when-let [to (get handbooks-nodes v)]
 | 
	
		
			
				|  |  | +                                 (nav-to-pane! [:topic-detail to (t :handbook/title)] [:dashboard]))
 | 
	
		
			
				|  |  | +                               (recur))))
 | 
	
		
			
				|  |  | +            #(async/go (>! c :return)))))
 | 
	
		
			
				|  |  | +      [handbooks-nodes])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      (fn []
 | 
	
		
			
				|  |  | +        (let [*cnt-len (atom 0)
 | 
	
		
			
				|  |  | +              check! (fn []
 | 
	
		
			
				|  |  | +                       (-> (p/let [^js res (js/fetch (get-handbooks-endpoint "/handbooks.edn") #js{:method "HEAD"})]
 | 
	
		
			
				|  |  | +                             (when-let [cl (.get (.-headers res) "content-length")]
 | 
	
		
			
				|  |  | +                               (when (not= @*cnt-len cl)
 | 
	
		
			
				|  |  | +                                 (println "[Handbooks] dev reload!")
 | 
	
		
			
				|  |  | +                                 (load-handbooks!))
 | 
	
		
			
				|  |  | +                               (reset! *cnt-len cl)))
 | 
	
		
			
				|  |  | +                           (p/catch #(println "[Handbooks] dev check Error:" %))))
 | 
	
		
			
				|  |  | +              timer0 (if dev-watch?
 | 
	
		
			
				|  |  | +                       (js/setInterval check! 2000) 0)]
 | 
	
		
			
				|  |  | +          #(js/clearInterval timer0)))
 | 
	
		
			
				|  |  | +      [dev-watch?])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    (rum/use-effect!
 | 
	
		
			
				|  |  | +      (fn []
 | 
	
		
			
				|  |  | +        (when handbooks-data
 | 
	
		
			
				|  |  | +          (let [nodes (->> (tree-seq map? :children handbooks-data)
 | 
	
		
			
				|  |  | +                           (reduce #(assoc %1 (or (:key %2) "__root") (bind-parent-key %2)) {}))]
 | 
	
		
			
				|  |  | +            (set-handbooks-nodes! nodes)
 | 
	
		
			
				|  |  | +            (set! (.-handbook-nodes js/window) (bean/->js nodes)))))
 | 
	
		
			
				|  |  | +      [handbooks-data])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    [:div.cp__handbooks-content
 | 
	
		
			
				|  |  | +     {:class     (util/classnames [{:search-active (:active? search-state)
 | 
	
		
			
				|  |  | +                                    :scrolled      scrolled?}])
 | 
	
		
			
				|  |  | +      :on-scroll on-scroll}
 | 
	
		
			
				|  |  | +     [:div.pane-wrap
 | 
	
		
			
				|  |  | +      [:div.hd.flex.justify-between.select-none.draggable-handle
 | 
	
		
			
				|  |  | +       [:h1.text-xl.flex.items-center.font-bold
 | 
	
		
			
				|  |  | +        (if pane-dashboard?
 | 
	
		
			
				|  |  | +          [:span (t :handbook/title)]
 | 
	
		
			
				|  |  | +          [:button.active:opacity-80.flex.items-center.cursor-pointer
 | 
	
		
			
				|  |  | +           {:on-click (fn [] (let [prev (first history-state)
 | 
	
		
			
				|  |  | +                                   prev (cond-> prev
 | 
	
		
			
				|  |  | +                                                (nil? (seq prev))
 | 
	
		
			
				|  |  | +                                                [:dashboard])]
 | 
	
		
			
				|  |  | +                               (set-active-pane-state! prev)
 | 
	
		
			
				|  |  | +                               (set-history-state! (rest history-state))))}
 | 
	
		
			
				|  |  | +           [:span.pr-2.flex.items-center (ui/icon "chevron-left")]
 | 
	
		
			
				|  |  | +           (let [title (or (last active-pane-state) (t :handbook/title) "")]
 | 
	
		
			
				|  |  | +             [:span.truncate.title {:title title} title])])]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +       [:div.flex.items-center.space-x-3
 | 
	
		
			
				|  |  | +        (when (> (count history-state) 1)
 | 
	
		
			
				|  |  | +          [:a.flex.items-center {:aria-label (t :handbook/home) :tabIndex "0" :on-click #(force-nav-dashboard!)} (ui/icon "home")])
 | 
	
		
			
				|  |  | +        (when pane-topic?
 | 
	
		
			
				|  |  | +          [:a.flex.items-center
 | 
	
		
			
				|  |  | +           {:aria-label "Copy topic link" :tabIndex "0"
 | 
	
		
			
				|  |  | +            :on-click   (fn []
 | 
	
		
			
				|  |  | +                          (let [s (str "logseq://handbook/" (:key (second active-pane-state)))]
 | 
	
		
			
				|  |  | +                            (util/copy-to-clipboard! s)
 | 
	
		
			
				|  |  | +                            (notification/show!
 | 
	
		
			
				|  |  | +                              [:div [:strong.block "Handbook link copied!"]
 | 
	
		
			
				|  |  | +                               [:label.opacity-50 s]] :success)))}
 | 
	
		
			
				|  |  | +           (ui/icon "copy")])
 | 
	
		
			
				|  |  | +        (when (state/developer-mode?)
 | 
	
		
			
				|  |  | +          [:a.flex.items-center {:aria-label (t :handbook/settings)
 | 
	
		
			
				|  |  | +                                 :tabIndex   "0"
 | 
	
		
			
				|  |  | +                                 :on-click   #(nav-to-pane! [:settings nil "Settings"] active-pane-state)}
 | 
	
		
			
				|  |  | +           (ui/icon "settings")])
 | 
	
		
			
				|  |  | +        [:a.flex.items-center {:aria-label (t :handbook/close) :tabIndex "0" :on-click #(state/toggle! :ui/handbooks-open?)}
 | 
	
		
			
				|  |  | +         (ui/icon "x")]]]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      (when (and (not pane-settings?) (not handbooks-loaded?))
 | 
	
		
			
				|  |  | +        [:div.flex.items-center.justify-center.pt-32
 | 
	
		
			
				|  |  | +         (if-not (:error handbooks-state)
 | 
	
		
			
				|  |  | +           (ui/loading "Loading ...")
 | 
	
		
			
				|  |  | +           [:code (:error handbooks-state)])])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      (when (or pane-settings? handbooks-loaded?)
 | 
	
		
			
				|  |  | +        [:<>
 | 
	
		
			
				|  |  | +         ;; search bar
 | 
	
		
			
				|  |  | +         (when (or pane-dashboard? (= :topics active-pane-name))
 | 
	
		
			
				|  |  | +           (search-bar active-pane-state nav-to-pane!
 | 
	
		
			
				|  |  | +                       handbooks-nodes search-state set-search-state!))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +         ;; entry pane
 | 
	
		
			
				|  |  | +         (when pane-render
 | 
	
		
			
				|  |  | +           (apply pane-render
 | 
	
		
			
				|  |  | +                  (case active-pane-name
 | 
	
		
			
				|  |  | +                    :settings
 | 
	
		
			
				|  |  | +                    [dev-watch? #(do (set-dev-watch? %)
 | 
	
		
			
				|  |  | +                                     (storage/set :handbooks-dev-watch? %))]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    ;; default inputs
 | 
	
		
			
				|  |  | +                    [handbooks-nodes active-pane-state nav-to-pane!])))])]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +     (when handbooks-loaded?
 | 
	
		
			
				|  |  | +       ;; footer
 | 
	
		
			
				|  |  | +       (when pane-dashboard?
 | 
	
		
			
				|  |  | +         [:div.ft
 | 
	
		
			
				|  |  | +          (footer-link-cards)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +          ;; TODO: how to get related topics?
 | 
	
		
			
				|  |  | +          ;(when (= :topic-detail active-pane)
 | 
	
		
			
				|  |  | +          ;  [:<>
 | 
	
		
			
				|  |  | +          ;   [:h2.uppercase.opacity-60 "Related"]
 | 
	
		
			
				|  |  | +          ;   (related-topics)])
 | 
	
		
			
				|  |  | +          ]))]))
 |