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:
Charlie
2023-10-27 14:33:10 +08:00
committed by GitHub
parent a67ed5c114
commit 1389836119
20 changed files with 1394 additions and 120 deletions

1
resources/js/glide/glide.core.min.css vendored Normal file
View 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

File diff suppressed because one or more lines are too long

View 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}

View File

@@ -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.")}))))

View File

@@ -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!
[]

View File

@@ -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)]

View File

@@ -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))])))

View File

@@ -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);
}
}
}

View 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?))

View File

@@ -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;

View 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)])
]))]))

View 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;
}
}
}
}
}

View File

@@ -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"
)

View File

@@ -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!
[]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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]

View File

@@ -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"