mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 22:25:01 +00:00
Feat: the new handbooks (#8524)
* feat(ui): WIP handbooks pane * feat(ui): WIP handbooks pane * feat(ui): WIP handbooks pane * feat(ui): WIP handbooks popup * feat(ui): WIP dragable & resizable for handbooks popup * feat(ui): WIP pane navigations for handbooks popup * feat(ui): WIP pane navigations for handbooks popup * feat(ui): WIP handbooks markdown body * feat(ui): WIP handbooks nodes for dashboard render * feat(ui): WIP watch mode for development * improve(ui): typos * feat(ui): WIP enhance watch mode * feat(ui): WIP support topic conent link local assets * feat(ui): WIP support slide gallery for demo images & videos. * fix(ui): parse value about draging position offset * improve(ui): background color transition of handbook item card * improve(ui): resizable of handbooks popup * feat(handbooks): search topics * improve(handbooks): search results within topics group * improve(ui): better interaction for handbooks searchbar * fix(handbooks): conflictive up/down for searchbar interaction * improve(ux): better interaction for handbooks searchbar * feat(ux): support youtube video for topic demos media * fix(ui): container size of youtube video demos * improve(handbooks): support local video for topic demos * improve(ui): polish markdown body style for handbooks topic details * chore: remove debugs * chore: remove debugs * improve(ui): polish active style for topic item card * improve(ui): polish style of demos item * improve(ui): help buttons still be visible when right sidebar opened * improve(handbooks): support sub chapters for topic detail * improve(handbooks): support sub chapters for topic detail * improve(handbooks): support chapters searching for topics list * fix: lint * improve(ui): position of demo slides bullets * fix(ui): index of chapter select * improve(handbooks): typo * fix(dev): lint * fix(dev): lint * fix(pdf): remove prefix(`@`) checking for links of org mode page * feat(ui): WIP handbooks pane * feat(ui): WIP handbooks pane * feat(ui): WIP handbooks pane * feat(ui): WIP handbooks popup * feat(ui): WIP dragable & resizable for handbooks popup * feat(ui): WIP pane navigations for handbooks popup * feat(ui): WIP pane navigations for handbooks popup * feat(ui): WIP handbooks markdown body * feat(ui): WIP handbooks nodes for dashboard render * feat(ui): WIP watch mode for development * improve(ui): typos * feat(ui): WIP enhance watch mode * feat(ui): WIP support topic conent link local assets * feat(ui): WIP support slide gallery for demo images & videos. * fix(ui): parse value about draging position offset * improve(ui): background color transition of handbook item card * improve(ui): resizable of handbooks popup * feat(handbooks): search topics * improve(handbooks): search results within topics group * improve(ui): better interaction for handbooks searchbar * fix(handbooks): conflictive up/down for searchbar interaction * improve(ux): better interaction for handbooks searchbar * feat(ux): support youtube video for topic demos media * fix(ui): container size of youtube video demos * improve(handbooks): support local video for topic demos * improve(ui): polish markdown body style for handbooks topic details * chore: remove debugs * chore: remove debugs * improve(ui): polish active style for topic item card * improve(ui): polish style of demos item * improve(ui): help buttons still be visible when right sidebar opened * improve(handbooks): support sub chapters for topic detail * improve(handbooks): support sub chapters for topic detail * improve(handbooks): support chapters searching for topics list * fix: lint * improve(ui): position of demo slides bullets * fix(ui): index of chapter select * improve(handbooks): typo * fix(dev): lint * fix(dev): lint * improve(handbook): i18n * fix(lint): unused translations * fix: accessibility issues and translations * fix(handbook): chapters navigation * enhance(handbook): ux of the chapters select * enhance(handbook): support link other page with markdown link syntax * improve(ui): polish ui details of handbook topics card * fix(handbook): parse key from href with a specific extension * enhance(handbook): logic of chapters navigation * enhance(handbook): ui of chapters navigation * fix: lint * improve(ui): display nowrap for code text * fix(handbook): remove unnecessary source map * fix(ui): missing component key of handbook chapter select * enhance(handbook): WIP support panes navigation for the external links * enhance(handbook): support panes navigation for the external links * improve(ui): footer links of the handbook home pane * improve(ui): footer links of the handbook home pane * improve(ui): polish topics card * improve(handbook): add shortcuts category card for home pane * improve(ui): WIP the new help menu * improve(ui): the new help menu * fix: incorrect help link * improve(ux): close help menu when click outside * fix: lint * fix(lint): remove unused translation * fix(ui): the link of changelog * fix(ui): the cover thumb container size of the topic card * fix(ui): handbook popup overlay index * enhance(ux): preivew images with lightbox modal for the handbook content * enhance(ux): bottom border for the handbook content header when then content body scrolled * fix: missing i18n * improve(handbook): polish ui details * fix: lint * enhance(handbook): polish details * fix(ui): incorrect safety init * fix(ui): missing key for the help menu items * enhance(ui): disable resize for the handbook popup container * chore: build libs core * fix(ui): incorrect shortcuts label * enhance(handbook): cache discord online number * enhance(handbook): fix heading level sizes * enhance(handbook): improve paragraph spacing * enhance(handbook): improve margins of media elements * enhance(handbook): polish discord button * enhance(plugin): make headings/font weights/colors look like in the design * enhance(handbook): writing mode option is only available for develop mode * enhance(handbook): polish handbook dashboard page * enhance(handbook): typos * enhance(ux): get discord online users count from logseq server * fix(handbooks): incorrect var name * enhance(handbook): polish details --------- Co-authored-by: Bad3r <bad3r@protonmail.com> Co-authored-by: situ2001 <yongcong2001@outlook.com> Co-authored-by: Tienson Qin <tiensonqin@gmail.com> Co-authored-by: Konstantinos Kaloutas <konstantinos@logseq.com>
This commit is contained in:
1
resources/js/glide/glide.core.min.css
vendored
Normal file
1
resources/js/glide/glide.core.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.glide{position:relative;width:100%;box-sizing:border-box}.glide *{box-sizing:inherit}.glide__track{overflow:hidden}.glide__slides{position:relative;width:100%;list-style:none;backface-visibility:hidden;transform-style:preserve-3d;touch-action:pan-Y;overflow:hidden;margin:0;padding:0;white-space:nowrap;display:flex;flex-wrap:nowrap;will-change:transform}.glide__slides--dragging{user-select:none}.glide__slide{width:100%;height:100%;flex-shrink:0;white-space:normal;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent}.glide__slide a{user-select:none;-webkit-user-drag:none;-moz-user-select:none;-ms-user-select:none}.glide__arrows{-webkit-touch-callout:none;user-select:none}.glide__bullets{-webkit-touch-callout:none;user-select:none}.glide--rtl{direction:rtl}
|
||||
6
resources/js/glide/glide.min.js
vendored
Normal file
6
resources/js/glide/glide.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/js/glide/glide.theme.min.css
vendored
Normal file
1
resources/js/glide/glide.theme.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.glide__arrow{position:absolute;display:block;top:50%;z-index:2;color:#fff;text-transform:uppercase;padding:9px 12px;background-color:transparent;border:2px solid rgba(255,255,255,.5);border-radius:4px;box-shadow:0 .25em .5em 0 rgba(0,0,0,.1);text-shadow:0 .25em .5em rgba(0,0,0,.1);opacity:1;cursor:pointer;transition:opacity 150ms ease,border 300ms ease-in-out;transform:translateY(-50%);line-height:1}.glide__arrow:focus{outline:none}.glide__arrow:hover{border-color:#fff}.glide__arrow--left{left:2em}.glide__arrow--right{right:2em}.glide__arrow--disabled{opacity:.33}.glide__bullets{position:absolute;z-index:2;bottom:2em;left:50%;display:inline-flex;list-style:none;transform:translateX(-50%)}.glide__bullet{background-color:rgba(255,255,255,.5);width:9px;height:9px;padding:0;border-radius:50%;border:2px solid transparent;transition:all 300ms ease-in-out;cursor:pointer;line-height:0;box-shadow:0 .25em .5em 0 rgba(0,0,0,.1);margin:0 .25em}.glide__bullet:focus{outline:none}.glide__bullet:hover,.glide__bullet:focus{border:2px solid #fff;background-color:rgba(255,255,255,.5)}.glide__bullet--active{background-color:#fff}.glide--swipeable{cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.glide--dragging{cursor:grabbing;cursor:-moz-grabbing;cursor:-webkit-grabbing}
|
||||
@@ -100,7 +100,13 @@
|
||||
(= "new-window" url-host)
|
||||
(local-url-handler win parsed-url true)
|
||||
|
||||
(= "handbook" url-host)
|
||||
(send-to-renderer :handbook
|
||||
{:key (some-> (.-pathname parsed-url) (string/replace-first #"^[\/]+" ""))
|
||||
:args (some-> (.-searchParams parsed-url) (js/Object.fromEntries))})
|
||||
|
||||
:else
|
||||
(send-to-renderer "notification" {:type "error"
|
||||
:payload (str "Failed to open link. Cannot match `" url-host
|
||||
"` to any target.")}))))
|
||||
(send-to-renderer :notification
|
||||
{:type "error"
|
||||
:payload (str "Failed to open link. Cannot match `" url-host
|
||||
"` to any target.")}))))
|
||||
|
||||
@@ -188,7 +188,13 @@
|
||||
|
||||
(safe-api-call "syncAPIServerState"
|
||||
(fn [^js data]
|
||||
(state/set-state! :electron/server (bean/->clj data)))))
|
||||
(state/set-state! :electron/server (bean/->clj data))))
|
||||
|
||||
|
||||
(safe-api-call "handbook"
|
||||
(fn [^js data]
|
||||
(when-let [k (and data (.-key data))]
|
||||
(state/open-handbook-pane! k)))))
|
||||
|
||||
(defn listen!
|
||||
[]
|
||||
|
||||
@@ -1338,7 +1338,7 @@
|
||||
url)]
|
||||
(if (and (coll? src)
|
||||
(= (first src) "youtube-player"))
|
||||
(youtube/youtube-video (last src))
|
||||
(youtube/youtube-video (last src) nil)
|
||||
(when src
|
||||
(let [width (min (- (util/get-width) 96) 560)
|
||||
height (int (* width (/ (if (string/includes? src "player.bilibili.com")
|
||||
@@ -1475,7 +1475,7 @@
|
||||
:else
|
||||
(nth (util/safe-re-find text-util/youtube-regex url) 5))]
|
||||
(when-not (string/blank? youtube-id)
|
||||
(youtube/youtube-video youtube-id))))
|
||||
(youtube/youtube-video youtube-id nil))))
|
||||
|
||||
(= name "youtube-timestamp")
|
||||
(when-let [timestamp (first arguments)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns frontend.components.container
|
||||
(:require [cljs-drag-n-drop.core :as dnd]
|
||||
[clojure.string :as string]
|
||||
[frontend.version :refer [version]]
|
||||
[frontend.components.find-in-page :as find-in-page]
|
||||
[frontend.components.header :as header]
|
||||
[frontend.components.journal :as journal]
|
||||
@@ -11,6 +12,7 @@
|
||||
[frontend.components.select :as select]
|
||||
[frontend.components.theme :as theme]
|
||||
[frontend.components.widgets :as widgets]
|
||||
[frontend.components.handbooks :as handbooks]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
@@ -35,6 +37,7 @@
|
||||
[frontend.util :as util]
|
||||
[frontend.util.cursor :as cursor]
|
||||
[frontend.components.window-controls :as window-controls]
|
||||
[medley.core :as medley]
|
||||
[goog.dom :as gdom]
|
||||
[goog.object :as gobj]
|
||||
[logseq.common.path :as path]
|
||||
@@ -696,15 +699,71 @@
|
||||
{:on-click state/toggle-document-mode!}
|
||||
"D"])))
|
||||
|
||||
(def help-menu-items
|
||||
[{:title "Handbook" :icon "book-2" :on-click #(handbooks/toggle-handbooks)}
|
||||
{:title "Keyboard shortcuts" :icon "command" :on-click #(state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings)}
|
||||
{:title "Documentation" :icon "help" :href "https://docs.logseq.com/"}
|
||||
:hr
|
||||
{:title "Report bug" :icon "bug" :on-click #(rfe/push-state :bug-report)}
|
||||
{:title "Request feature" :icon "git-pull-request" :href "https://discuss.logseq.com/c/feature-requests/"}
|
||||
{:title "Submit feedback" :icon "messages" :href "https://discuss.logseq.com/c/feedback/13"}
|
||||
:hr
|
||||
{:title "Ask the community" :icon "brand-discord" :href "https://discord.com/invite/KpN4eHY"}
|
||||
{:title "Support forum" :icon "message" :href "https://discuss.logseq.com/"}
|
||||
:hr
|
||||
{:title "Release notes" :icon "asterisk" :href "https://docs.logseq.com/#/page/changelog"}])
|
||||
|
||||
(rum/defc help-menu-popup
|
||||
[]
|
||||
|
||||
(rum/use-effect!
|
||||
(fn []
|
||||
(state/set-state! :ui/handbooks-open? false))
|
||||
[])
|
||||
|
||||
(rum/use-effect!
|
||||
(fn []
|
||||
(let [h #(state/set-state! :ui/help-open? false)]
|
||||
(.addEventListener js/document.body "click" h)
|
||||
#(.removeEventListener js/document.body "click" h)))
|
||||
[])
|
||||
|
||||
[:div.cp__sidebar-help-menu-popup
|
||||
[:div.list-wrap
|
||||
(for [[idx {:keys [title icon href on-click] :as item}] (medley/indexed help-menu-items)]
|
||||
(case item
|
||||
:hr
|
||||
[:hr.my-2 {:key idx}]
|
||||
|
||||
;; default
|
||||
[:a.it.flex.items-center.px-4.py-1.select-none
|
||||
{:key title
|
||||
:on-click (fn []
|
||||
(cond
|
||||
(fn? on-click) (on-click)
|
||||
(string? href) (util/open-url href))
|
||||
(state/set-state! :ui/help-open? false))}
|
||||
[:span.flex.items-center.pr-2.opacity-40 (ui/icon icon {:size 20})]
|
||||
[:strong.font-normal title]]))]
|
||||
[:div.ft.pl-11.pb-3
|
||||
[:span.opacity.text-xs.opacity-30 "Logseq " version]]])
|
||||
|
||||
(rum/defc help-button < rum/reactive
|
||||
[]
|
||||
(when-not (state/sub :ui/sidebar-open?)
|
||||
[:div.cp__sidebar-help-btn
|
||||
[:div.inner
|
||||
{:title (t :help-shortcut-title)
|
||||
:on-click (fn []
|
||||
(state/sidebar-add-block! (state/get-current-repo) "help" :help))}
|
||||
"?"]]))
|
||||
(let [help-open? (state/sub :ui/help-open?)
|
||||
handbooks-open? (state/sub :ui/handbooks-open?)]
|
||||
[:<>
|
||||
[:div.cp__sidebar-help-btn
|
||||
[:div.inner
|
||||
{:title (t :help-shortcut-title)
|
||||
:on-click #(state/toggle! :ui/help-open?)}
|
||||
"?"]]
|
||||
|
||||
(when help-open?
|
||||
(help-menu-popup))
|
||||
|
||||
(when handbooks-open?
|
||||
(handbooks/handbooks-popup))]))
|
||||
|
||||
(rum/defcs ^:large-vars/cleanup-todo sidebar <
|
||||
(mixins/modal :modal/show?)
|
||||
@@ -828,7 +887,6 @@
|
||||
:nfs-granted? granted?
|
||||
:db-restoring? db-restoring?})
|
||||
[:a#download.hidden]
|
||||
(when
|
||||
(and (not config/mobile?)
|
||||
(not config/publishing?))
|
||||
(when (and (not config/mobile?)
|
||||
(not config/publishing?))
|
||||
(help-button))])))
|
||||
|
||||
@@ -487,14 +487,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
#left-sidebar {
|
||||
> .shade-mask {
|
||||
background-color: rgba(0, 0, 0, .15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-modal {
|
||||
@apply -m-8 rounded-lg;
|
||||
/* box-shadow: inset 0 0 0 1px var(--ls-border-color); */
|
||||
@@ -527,13 +519,36 @@ html[data-theme='dark'] {
|
||||
@apply fixed bottom-4 right-4 sm:right-8;
|
||||
|
||||
> .inner {
|
||||
@apply font-bold
|
||||
rounded-full h-8 w-8 flex items-center justify-center font-bold
|
||||
select-none cursor-help;
|
||||
@apply rounded-full h-8 w-8 flex items-center justify-center
|
||||
font-bold select-none cursor-help;
|
||||
|
||||
background-color: var(--ls-secondary-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
&-menu-popup {
|
||||
@apply fixed bottom-14 right-8 z-10 border
|
||||
rounded-lg min-w-[260px] shadow;
|
||||
|
||||
background-color: var(--ls-secondary-background-color);
|
||||
border-color: var(--ls-border-color);
|
||||
|
||||
> .list-wrap {
|
||||
@apply flex flex-col pt-3;
|
||||
|
||||
.it {
|
||||
color: var(--ls-primary-text-color);
|
||||
|
||||
&:active, &:hover {
|
||||
background-color: var(--ls-tertiary-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-handbook-btn {
|
||||
@apply bottom-16;
|
||||
}
|
||||
}
|
||||
|
||||
.cp__right-sidebar {
|
||||
@@ -754,3 +769,11 @@ a.ui__modal-close, a.close {
|
||||
a.ui__modal-close:hover, a.close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
#left-sidebar {
|
||||
> .shade-mask {
|
||||
background-color: rgba(0, 0, 0, .15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
src/main/frontend/components/handbooks.cljs
Normal file
40
src/main/frontend/components/handbooks.cljs
Normal file
@@ -0,0 +1,40 @@
|
||||
(ns frontend.components.handbooks
|
||||
(:require [rum.core :as rum]
|
||||
[frontend.state :as state]
|
||||
[frontend.modules.layout.core :as layout]
|
||||
;[shadow.lazy :as lazy]
|
||||
[frontend.extensions.handbooks.core :as handbooks]))
|
||||
|
||||
#_:clj-kondo/ignore
|
||||
;(def lazy-handbooks (lazy/loadable frontend.extensions.handbooks.core/content))
|
||||
;
|
||||
;(rum/defc loadable-handbooks
|
||||
; []
|
||||
; (let [[content set-content] (rum/use-state nil)]
|
||||
;
|
||||
; (rum/use-effect!
|
||||
; (fn []
|
||||
; (lazy/load lazy-handbooks #(set-content %))) [])
|
||||
;
|
||||
; [:div.cp__handbooks-content
|
||||
; content]))
|
||||
|
||||
(rum/defc handbooks-popup
|
||||
[]
|
||||
(let [popup-ref (rum/use-ref nil)]
|
||||
(rum/use-effect!
|
||||
(fn []
|
||||
(when-let [^js popup-el (rum/deref popup-ref)]
|
||||
(comp
|
||||
(layout/setup-draggable-container! popup-el nil))))
|
||||
[])
|
||||
|
||||
[:div.cp__handbooks-popup
|
||||
{:data-identity "logseq-handbooks"
|
||||
:ref popup-ref}
|
||||
[:div.cp__handbooks-content-wrap
|
||||
(handbooks/content)]]))
|
||||
|
||||
(defn toggle-handbooks
|
||||
[]
|
||||
(state/toggle! :ui/handbooks-open?))
|
||||
@@ -506,6 +506,7 @@ body[data-page=import] {
|
||||
|
||||
&.cp__sidebar-help-btn {
|
||||
background-color: rgba(0, 0, 0, .4);
|
||||
z-index: 9;
|
||||
|
||||
> .inner {
|
||||
opacity: 1;
|
||||
|
||||
658
src/main/frontend/extensions/handbooks/core.cljs
Normal file
658
src/main/frontend/extensions/handbooks/core.cljs
Normal file
@@ -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)])
|
||||
]))]))
|
||||
412
src/main/frontend/extensions/handbooks/handbooks.css
Normal file
412
src/main/frontend/extensions/handbooks/handbooks.css
Normal file
@@ -0,0 +1,412 @@
|
||||
.cp__handbooks {
|
||||
&-content {
|
||||
@apply flex flex-col justify-between flex-1 overflow-y-auto;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-y: overlay;
|
||||
|
||||
&-wrap {
|
||||
@apply flex justify-center flex-col flex-1 h-full overflow-y-auto relative;
|
||||
}
|
||||
|
||||
.hd {
|
||||
@apply dark:text-white px-3 pt-3 pb-2 sticky top-0 left-0 z-[4]
|
||||
transition-shadow duration-200;
|
||||
|
||||
background-color: var(--ls-tertiary-background-color);
|
||||
|
||||
.title {
|
||||
text-align: left;
|
||||
width: 266px;
|
||||
}
|
||||
}
|
||||
|
||||
&.scrolled {
|
||||
.hd {
|
||||
box-shadow: -3px 4px 6px -6px #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
@apply flex flex-col pb-[6px] mb-0;
|
||||
|
||||
> .input-wrap {
|
||||
@apply mx-4 mb-2 flex rounded-lg mt-1.5;
|
||||
|
||||
border: 3px solid var(--ls-primary-background-color);
|
||||
background-color: var(--ls-primary-background-color);
|
||||
|
||||
&:focus-within {
|
||||
border: 3px solid var(--ls-secondary-border-color);
|
||||
}
|
||||
|
||||
> input {
|
||||
@apply text-base leading-none w-full border-none py-[7px] px-[24px] bg-transparent
|
||||
focus:outline-0 dark:text-gray-100 font-medium;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
> .search-results-wrap {
|
||||
@apply px-4 py-1;
|
||||
}
|
||||
}
|
||||
|
||||
.pane {
|
||||
@apply py-1 px-4 dark:text-gray-50;
|
||||
}
|
||||
|
||||
.pane > h2, .ft > h2 {
|
||||
@apply py-2 text-base font-medium dark:text-gray-100;
|
||||
}
|
||||
|
||||
.ft {
|
||||
@apply px-4 pt-4 pb-2;
|
||||
|
||||
background-color: var(--ls-quaternary-background-color);
|
||||
|
||||
/*noinspection ALL*/
|
||||
|
||||
svg {
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-card, .link-card {
|
||||
@apply text-sm px-3 py-2.5 rounded-lg cursor-pointer
|
||||
mb-2 active:opacity-90 select-none items-center;
|
||||
|
||||
background-color: var(--ls-secondary-background-color);
|
||||
border: 1px solid var(--ls-border-color);
|
||||
transition: background-color .3s;
|
||||
|
||||
> .l {
|
||||
@apply pr-2.5 w-[80px] min-h-[64px] bg-transparent rounded overflow-hidden;
|
||||
|
||||
img {
|
||||
mix-blend-mode: luminosity;
|
||||
opacity: .8;
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> .r {
|
||||
@apply leading-none flex-1;
|
||||
|
||||
> strong {
|
||||
@apply font-medium text-sm pt-0.5 pb-[1px] opacity-90 leading-5 dark:text-gray-5;
|
||||
}
|
||||
|
||||
> span {
|
||||
@apply text-xs opacity-40 leading-4;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &.active {
|
||||
background-color: var(--ls-primary-background-color);
|
||||
border-color: var(--ls-secondary-border-color);
|
||||
|
||||
> .l {
|
||||
img {
|
||||
mix-blend-mode: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-card {
|
||||
@apply dark:text-gray-100;
|
||||
|
||||
border-color: var(--ls-tertiary-border-color);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--ls-tertiary-border-color);
|
||||
}
|
||||
|
||||
&.as-primary {
|
||||
@apply bg-indigo-500 text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.category-card {
|
||||
@apply flex rounded px-2 py-3 active:opacity-90 cursor-pointer transition-colors items-end;
|
||||
|
||||
border-left: 4px solid var(--ls-secondary-background-color);
|
||||
background-color: var(--ls-secondary-background-color);
|
||||
|
||||
&[data-total="0"] {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
&:hover, &:active {
|
||||
background-color: var(--ls-primary-background-color);
|
||||
}
|
||||
|
||||
> .icon-wrap {
|
||||
@apply flex justify-end pr-2 pb-[2px] opacity-20;
|
||||
}
|
||||
|
||||
> .text-wrap {
|
||||
@apply flex flex-col min-h-[48px] justify-end;
|
||||
|
||||
> strong {
|
||||
@apply font-medium leading-tight text-sm;
|
||||
}
|
||||
|
||||
> span {
|
||||
@apply text-xs pt-1;
|
||||
|
||||
color: var(--ls-primary-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.categories-list {
|
||||
@apply grid grid-cols-2 gap-3;
|
||||
}
|
||||
|
||||
.pane-topic-detail {
|
||||
@apply flex flex-col h-full;
|
||||
|
||||
> h1 {
|
||||
@apply pb-1;
|
||||
}
|
||||
|
||||
> .demos {
|
||||
img, video {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
&.glide {
|
||||
@apply mb-[10px];
|
||||
}
|
||||
|
||||
.glide__slide {
|
||||
background-color: var(--ls-secondary-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@apply overflow-hidden pt-1 leading-6;
|
||||
|
||||
&-wrap {
|
||||
@apply flex flex-col justify-around;
|
||||
}
|
||||
}
|
||||
|
||||
iframe {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.glide {
|
||||
&__bullets {
|
||||
@apply w-full bottom-0 left-0 transform-none
|
||||
flex items-center justify-end pb-2 pr-1;
|
||||
}
|
||||
|
||||
&__bullet {
|
||||
@apply dark:text-black;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
&--swipeable {
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.search-active {
|
||||
.pane {
|
||||
&:not(.pane-topic-detail) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ft {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
@apply pt-4;
|
||||
|
||||
-webkit-font-smoothing: initial;
|
||||
|
||||
h1 {
|
||||
@apply py-1 text-2xl font-bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply py-1 text-xl font-bold;
|
||||
}
|
||||
|
||||
h3, h4 {
|
||||
@apply py-1 text-lg font-semibold;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply py-0.5 font-semibold text-sm;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply py-0.5 text-xs font-semibold;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply leading-[1.6rem] my-[0.75rem];
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chapters {
|
||||
&-wrap {
|
||||
|
||||
}
|
||||
|
||||
&-select {
|
||||
.select-trigger {
|
||||
@apply relative flex flex-col rounded py-2 px-3 leading-5 select-none z-[1];
|
||||
|
||||
color: var(--ls-primary-text-color);
|
||||
background-color: var(--ls-secondary-background-color);
|
||||
|
||||
small {
|
||||
@apply text-[11px] opacity-50 pl-0.5;
|
||||
}
|
||||
|
||||
strong {
|
||||
@apply text-sm dark:text-gray-100;
|
||||
}
|
||||
|
||||
.ui__icon {
|
||||
@apply absolute right-2 top-5 opacity-70;
|
||||
}
|
||||
|
||||
&:active {
|
||||
.ui__icon, strong {
|
||||
@apply opacity-80;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply absolute top-[58px] left-0 w-full list-none m-0 rounded-b py-2;
|
||||
|
||||
background-color: var(--ls-secondary-background-color);
|
||||
transform: translateY(-5px);
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
|
||||
li {
|
||||
@apply list-none px-3 py-1 transition-colors text-sm;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ls-tertiary-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
img {
|
||||
@apply cursor-pointer active:opacity-80;
|
||||
}
|
||||
|
||||
img, video {
|
||||
@apply inline-block my-1;
|
||||
}
|
||||
}
|
||||
|
||||
&-popup {
|
||||
@apply fixed rounded-lg overflow-hidden
|
||||
z-[19] shadow-lg flex justify-center flex-col;
|
||||
|
||||
background-color: var(--ls-tertiary-background-color);
|
||||
border: 1px solid var(--ls-tertiary-background-color);
|
||||
touch-action: none;
|
||||
height: 686px;
|
||||
max-height: 86vh;
|
||||
width: 420px;
|
||||
right: 32px;
|
||||
bottom: 58px;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
.cp__handbooks-popup {
|
||||
background-color: var(--ls-primary-background-color);
|
||||
|
||||
.input-wrap {
|
||||
background-color: #f1f1f1;
|
||||
|
||||
&:focus-within {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-card, :not(.as-primary).link-card {
|
||||
&:hover, &.active {
|
||||
background-color: var(--ls-tertiary-background-color);
|
||||
border-color: var(--ls-secondary-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cp__handbooks-content {
|
||||
.hd {
|
||||
background-color: var(--ls-primary-background-color);
|
||||
}
|
||||
|
||||
.ft {
|
||||
background-color: var(--ls-primary-background-color);
|
||||
}
|
||||
|
||||
.search {
|
||||
background-color: var(--ls-primary-background-color);
|
||||
}
|
||||
|
||||
.chapters-select {
|
||||
.select-trigger {
|
||||
background-color: var(--ls-tertiary-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.categories-list {
|
||||
.category-card {
|
||||
&:hover, &:active {
|
||||
background-color: var(--ls-tertiary-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: unset;
|
||||
|
||||
ul {
|
||||
list-style: circle;
|
||||
|
||||
ul {
|
||||
list-style: square;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,17 +28,17 @@
|
||||
|
||||
(defn register-player [state]
|
||||
(try
|
||||
(let [id (first (:rum/args state))
|
||||
node (rum/dom-node state)]
|
||||
(when node
|
||||
(let [player (js/window.YT.Player.
|
||||
node
|
||||
(clj->js
|
||||
{:events
|
||||
{"onReady" (fn [_e] (js/console.log id " ready"))}}))]
|
||||
(state/update-state! [:youtube/players]
|
||||
(fn [players]
|
||||
(assoc players id player))))))
|
||||
(let [id (first (:rum/args state))
|
||||
node (rum/dom-node state)]
|
||||
(when node
|
||||
(let [player (js/window.YT.Player.
|
||||
node
|
||||
(clj->js
|
||||
{:events
|
||||
{"onReady" (fn [_e] (js/console.log id " ready"))}}))]
|
||||
(state/update-state! [:youtube/players]
|
||||
(fn [players]
|
||||
(assoc players id player))))))
|
||||
(catch :default _e
|
||||
nil)))
|
||||
|
||||
@@ -51,14 +51,14 @@
|
||||
(<! (load-youtube-api))
|
||||
(register-player state))
|
||||
state)}
|
||||
[state id]
|
||||
(let [width (min (- (util/get-width) 96)
|
||||
560)
|
||||
height (int (* width (/ 315 560)))]
|
||||
[state id {:keys [width height] :as _opts}]
|
||||
(let [width (or width (min (- (util/get-width) 96)
|
||||
560))
|
||||
height (or height (int (* width (/ 315 560))))]
|
||||
[:iframe
|
||||
{:id (str "youtube-player-" id)
|
||||
:allow-full-screen "allowfullscreen"
|
||||
:allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
|
||||
:allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
|
||||
:frame-border "0"
|
||||
:src (str "https://www.youtube.com/embed/" id "?enablejsapi=1")
|
||||
:height height
|
||||
@@ -141,13 +141,13 @@ Remember: You can paste a raw YouTube url as embedded video on mobile."
|
||||
(re-matches #"^(?:(\d+):)?([0-5]?\d):([0-5]?\d)$" "123:22:23") ;; => ["123:22:23" "123" "22" "23"]
|
||||
(re-matches #"^(?:(\d+):)?([0-5]?\d):([0-5]?\d)$" "30:23") ;; => ["30:23" nil "30" "23"]
|
||||
|
||||
(parse-timestamp "01:23") ;; => 83
|
||||
(parse-timestamp "01:23") ;; => 83
|
||||
|
||||
(parse-timestamp "01:01:23") ;; => 3683
|
||||
(parse-timestamp "01:01:23") ;; => 3683
|
||||
|
||||
;; seconds->display
|
||||
;; https://stackoverflow.com/questions/1322732/convert-seconds-to-hh-mm-ss-with-javascript
|
||||
(seconds->display 129600) ;; => "36:00:00"
|
||||
(seconds->display 13545) ;; => "03:45:45"
|
||||
(seconds->display 18) ;; => "00:18"
|
||||
)
|
||||
;; seconds->display
|
||||
;; https://stackoverflow.com/questions/1322732/convert-seconds-to-hh-mm-ss-with-javascript
|
||||
(seconds->display 129600) ;; => "36:00:00"
|
||||
(seconds->display 13545) ;; => "03:45:45"
|
||||
(seconds->display 18) ;; => "00:18"
|
||||
)
|
||||
|
||||
@@ -68,11 +68,7 @@
|
||||
|
||||
(defn toggle-help!
|
||||
[]
|
||||
(when-let [current-repo (state/get-current-repo)]
|
||||
(let [id "help"]
|
||||
(if (state/sidebar-block-exists? id)
|
||||
(state/sidebar-remove-block! id)
|
||||
(state/sidebar-add-block! current-repo id :help)))))
|
||||
(state/toggle! :ui/help-open?))
|
||||
|
||||
(defn toggle-settings-modal!
|
||||
[]
|
||||
|
||||
@@ -16,19 +16,19 @@
|
||||
[identity]
|
||||
(when-let [^js/HTMLElement container (and (> (count @*movable-containers) 1)
|
||||
(get @*movable-containers identity))]
|
||||
(let [zdx (->> @*movable-containers
|
||||
(map (fn [[_ ^js el]]
|
||||
(let [^js c (js/getComputedStyle el)
|
||||
v1 (.-visibility c)
|
||||
v2 (.-display c)]
|
||||
(when-let [z (and (= "visible" v1)
|
||||
(not= "none" v2)
|
||||
(.-zIndex c))]
|
||||
z))))
|
||||
(remove nil?))
|
||||
zdx (bean/->js zdx)
|
||||
zdx (and zdx (js/Math.max.apply nil zdx))
|
||||
zdx' (util/safe-parse-int (.. container -style -zIndex))]
|
||||
(let [zdx (->> @*movable-containers
|
||||
(map (fn [[_ ^js el]]
|
||||
(let [^js c (js/getComputedStyle el)
|
||||
v1 (.-visibility c)
|
||||
v2 (.-display c)]
|
||||
(when-let [z (and (= "visible" v1)
|
||||
(not= "none" v2)
|
||||
(.-zIndex c))]
|
||||
z))))
|
||||
(remove nil?))
|
||||
zdx (bean/->js zdx)
|
||||
zdx (and zdx (js/Math.max.apply nil zdx))
|
||||
zdx' (some-> (.. container -style -zIndex) (parse-long))]
|
||||
|
||||
(when (or (nil? zdx') (not= zdx zdx'))
|
||||
(set! (.. container -style -zIndex) (inc zdx))))))
|
||||
@@ -36,35 +36,38 @@
|
||||
(defn ^:export setup-draggable-container!
|
||||
[^js/HTMLElement el callback]
|
||||
(when-let [^js/HTMLElement handle (.querySelector el ".draggable-handle")]
|
||||
(let [^js cls (.-classList el)
|
||||
^js ds (.-dataset el)
|
||||
(let [^js cls (.-classList el)
|
||||
^js ds (.-dataset el)
|
||||
identity (.-identity ds)
|
||||
ing? "is-dragging"]
|
||||
ing? "is-dragging"]
|
||||
|
||||
;; draggable
|
||||
(-> (js/interact handle)
|
||||
(.draggable
|
||||
(bean/->js
|
||||
{:listeners
|
||||
{:move (fn [^js/MouseEvent e]
|
||||
(let [^js dset (.-dataset el)
|
||||
dx (.-dx e)
|
||||
dy (.-dy e)
|
||||
dx' (util/safe-parse-float (.-dx dset))
|
||||
dy' (util/safe-parse-float (.-dy dset))
|
||||
x (+ dx (if dx' dx' 0))
|
||||
y (+ dy (if dy' dy' 0))]
|
||||
(bean/->js
|
||||
{:listeners
|
||||
{:move (fn [^js/MouseEvent e]
|
||||
(let [^js dset (.-dataset el)
|
||||
dx (.-dx e)
|
||||
dy (.-dy e)
|
||||
dx' (.-dx dset)
|
||||
dy' (.-dy dset)
|
||||
dx' (and dx' (util/safe-parse-float dx'))
|
||||
dy' (and dy' (util/safe-parse-float dy'))
|
||||
x (+ dx (or dx' 0))
|
||||
y (+ dy (or dy' 0))]
|
||||
|
||||
;; update container position
|
||||
(set! (.. el -style -transform) (str "translate(" x "px, " y "px)"))
|
||||
;; update container position
|
||||
(set! (.. el -style -transform) (str "translate(" x "px, " y "px)"))
|
||||
|
||||
;; cache dx dy
|
||||
(set! (.. el -dataset -dx) x)
|
||||
(set! (.. el -dataset -dy) y)))}}))
|
||||
;; cache dx dy
|
||||
(set! (.. el -dataset -dx) x)
|
||||
(set! (.. el -dataset -dy) y)))}}))
|
||||
(.on "dragstart" (fn [] (.add cls ing?)))
|
||||
(.on "dragend" (fn [e]
|
||||
(.remove cls ing?)
|
||||
(callback (bean/->js (calc-layout-data el e))))))
|
||||
(when (fn? callback)
|
||||
(callback (bean/->js (calc-layout-data el e)))))))
|
||||
;; manager
|
||||
(swap! *movable-containers assoc identity el)
|
||||
|
||||
@@ -72,45 +75,50 @@
|
||||
|
||||
(defn ^:export setup-resizable-container!
|
||||
[^js/HTMLElement el callback]
|
||||
(let [^js cls (.-classList el)
|
||||
^js ds (.-dataset el)
|
||||
(let [^js cls (.-classList el)
|
||||
^js ds (.-dataset el)
|
||||
identity (.-identity ds)
|
||||
ing? "is-resizing"]
|
||||
ing? "is-resizing"]
|
||||
|
||||
;; resizable
|
||||
(-> (js/interact el)
|
||||
(.resizable
|
||||
(bean/->js
|
||||
{:edges
|
||||
{:left true :top true :bottom true :right true}
|
||||
(bean/->js
|
||||
{:edges
|
||||
{:left true :top true :bottom true :right true}
|
||||
|
||||
:listeners
|
||||
{:start (fn [] (.add cls ing?))
|
||||
:end (fn [e] (.remove cls ing?) (callback (bean/->js (calc-layout-data el e))))
|
||||
:move (fn [^js/MouseEvent e]
|
||||
(let [^js dset (.-dataset el)
|
||||
w (.. e -rect -width)
|
||||
h (.. e -rect -height)
|
||||
:listeners
|
||||
{:start (fn [] (.add cls ing?))
|
||||
:end (fn [e]
|
||||
(.remove cls ing?)
|
||||
(when (fn? callback)
|
||||
(callback (bean/->js (calc-layout-data el e)))))
|
||||
:move (fn [^js/MouseEvent e]
|
||||
(let [^js dset (.-dataset el)
|
||||
w (.. e -rect -width)
|
||||
h (.. e -rect -height)
|
||||
|
||||
;; update position from top/left
|
||||
dx (.. e -deltaRect -left)
|
||||
dy (.. e -deltaRect -top)
|
||||
;; update position from top/left
|
||||
dx (.. e -deltaRect -left)
|
||||
dy (.. e -deltaRect -top)
|
||||
|
||||
dx' (util/safe-parse-float (.-dx dset))
|
||||
dy' (util/safe-parse-float (.-dy dset))
|
||||
dx' (.-dx dset)
|
||||
dy' (.-dy dset)
|
||||
dx' (and dx' (util/safe-parse-float dx'))
|
||||
dy' (and dy' (util/safe-parse-float dy'))
|
||||
|
||||
x (+ dx (if dx' dx' 0))
|
||||
y (+ dy (if dy' dy' 0))]
|
||||
x (+ dx (or dx' 0))
|
||||
y (+ dy (or dy' 0))]
|
||||
|
||||
;; update container position
|
||||
(set! (.. el -style -transform) (str "translate(" x "px, " y "px)"))
|
||||
;; update container position
|
||||
(set! (.. el -style -transform) (str "translate(" x "px, " y "px)"))
|
||||
|
||||
;; update container size
|
||||
(set! (.. el -style -width) (str w "px"))
|
||||
(set! (.. el -style -height) (str h "px"))
|
||||
;; update container size
|
||||
(set! (.. el -style -width) (str w "px"))
|
||||
(set! (.. el -style -height) (str h "px"))
|
||||
|
||||
(set! (. dset -dx) x)
|
||||
(set! (. dset -dy) y)))}})))
|
||||
(set! (. dset -dx) x)
|
||||
(set! (. dset -dy) y)))}})))
|
||||
|
||||
;; manager
|
||||
(swap! *movable-containers assoc identity el)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"Provides main application state, fns associated to set and state based rum
|
||||
cursors"
|
||||
(:require [cljs-bean.core :as bean]
|
||||
[cljs.core.async :as async :refer [<!]]
|
||||
[cljs.core.async :as async :refer [<! >!]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.string :as string]
|
||||
[dommy.core :as dom]
|
||||
@@ -74,6 +74,9 @@
|
||||
:ui/navigation-item-collapsed? {}
|
||||
|
||||
;; right sidebar
|
||||
:ui/handbooks-open? false
|
||||
:ui/help-open? false
|
||||
:ui/fullscreen? false
|
||||
:ui/settings-open? false
|
||||
:ui/sidebar-open? false
|
||||
:ui/sidebar-width "40%"
|
||||
@@ -285,6 +288,8 @@
|
||||
:graph/importing nil
|
||||
:graph/importing-state {}
|
||||
|
||||
:handbook/route-chan (async/chan (async/sliding-buffer 1))
|
||||
|
||||
:whiteboard/onboarding-whiteboard? (or (storage/get :ls-onboarding-whiteboard?) false)
|
||||
:whiteboard/onboarding-tour? (or (storage/get :whiteboard-onboarding-tour?) false)
|
||||
:whiteboard/last-persisted-at {}
|
||||
@@ -2190,6 +2195,21 @@ Similar to re-frame subscriptions"
|
||||
[]
|
||||
(storage/remove :user-groups))
|
||||
|
||||
(defn handbook-open?
|
||||
[]
|
||||
(:ui/handbooks-open? @state))
|
||||
|
||||
(defn get-handbook-route-chan
|
||||
[]
|
||||
(:handbook/route-chan @state))
|
||||
|
||||
(defn open-handbook-pane!
|
||||
[k]
|
||||
(when-not (handbook-open?)
|
||||
(set-state! :ui/handbooks-open? true))
|
||||
(js/setTimeout #(async/go
|
||||
(>! (get-handbook-route-chan) k))))
|
||||
|
||||
(defn set-page-properties-changed!
|
||||
[page-name]
|
||||
(when-not (string/blank? page-name)
|
||||
|
||||
@@ -64,6 +64,12 @@
|
||||
"purple"
|
||||
"gray"])
|
||||
|
||||
(defn ->block-background-color
|
||||
[color]
|
||||
(if (some #{color} built-in-colors)
|
||||
(str "var(--ls-highlight-color-" color ")")
|
||||
color))
|
||||
|
||||
(defn built-in-color?
|
||||
[color]
|
||||
(some #{color} built-in-colors))
|
||||
@@ -1038,7 +1044,7 @@
|
||||
:as option}]
|
||||
(let [klass (if-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700.text-center" intent)
|
||||
klass (if background (string/replace klass "indigo" background) klass)
|
||||
klass (if small? (str klass ".px-2.py-1") klass)
|
||||
klass (if small? (str klass ".is-small") klass)
|
||||
klass (if large? (str klass ".text-base") klass)
|
||||
klass (if disabled? (str klass "disabled:opacity-75") klass)]
|
||||
[:button.ui__button
|
||||
|
||||
@@ -275,7 +275,7 @@ html.is-mobile {
|
||||
|
||||
.ui__button {
|
||||
@apply inline-flex items-center px-3 py-2 border border-transparent
|
||||
text-sm leading-4 font-medium rounded-md text-white
|
||||
text-sm leading-4 font-medium rounded-[6px] text-white
|
||||
focus:outline-none transition ease-in-out duration-150;
|
||||
|
||||
&:disabled {
|
||||
@@ -317,8 +317,8 @@ html.is-mobile {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
&.p-1 {
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
&.is-small {
|
||||
@apply px-2.5 py-1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -225,6 +225,13 @@
|
||||
[pred coll]
|
||||
(first (filter pred coll)))
|
||||
|
||||
(defn find-index
|
||||
"Find first index of an element in list"
|
||||
[pred-or-val coll]
|
||||
(let [pred (if (fn? pred-or-val) pred-or-val #(= pred-or-val %))]
|
||||
(reduce-kv #(if (pred %3) (reduced %2) %1) -1
|
||||
(cond-> coll (list? coll) (vec)))))
|
||||
|
||||
;; (defn format
|
||||
;; [fmt & args]
|
||||
;; (apply gstring/format fmt args))
|
||||
@@ -1435,6 +1442,23 @@
|
||||
(fn [resolve]
|
||||
(load url resolve)))))
|
||||
|
||||
#?(:cljs
|
||||
(defn css-load$
|
||||
([url] (css-load$ url nil))
|
||||
([url id]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(let [id (str "css-load-" (or id url))]
|
||||
(if-not (gdom/getElement id)
|
||||
(let [^js link (js/document.createElement "link")]
|
||||
(set! (.-id link) id)
|
||||
(set! (.-rel link) "stylesheet")
|
||||
(set! (.-href link) url)
|
||||
(set! (.-onload link) resolve)
|
||||
(set! (.-onerror link) reject)
|
||||
(.append (.-head js/document) link))
|
||||
(resolve))))))))
|
||||
|
||||
#?(:cljs
|
||||
(defn copy-image-to-clipboard
|
||||
[src]
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
:on-boarding/tour-whiteboard-home-description "Whiteboards have their own section in the app where you can see them at a glance, create new ones or delete them easily."
|
||||
:on-boarding/tour-whiteboard-new "{1} Create new whiteboard"
|
||||
:on-boarding/tour-whiteboard-new-description "There are multiple ways of creating a new whiteboard. One of them is always right here in the dashboard."
|
||||
:handbook/title "Help"
|
||||
:handbook/topics "Topics"
|
||||
:handbook/popular-topics "Popular topics"
|
||||
:handbook/help-categories "Help categories"
|
||||
:handbook/search "Search"
|
||||
:handbook/home "Home"
|
||||
:handbook/settings "Settings"
|
||||
:handbook/close "Close"
|
||||
:on-boarding/tour-whiteboard-btn-next "Next"
|
||||
:on-boarding/tour-whiteboard-btn-back "Back"
|
||||
:on-boarding/tour-whiteboard-btn-finish "Finish"
|
||||
|
||||
Reference in New Issue
Block a user