💄 Update comment UI with new design

This commit is contained in:
Eva 2023-10-13 15:00:50 +02:00
parent 25c60f3e0f
commit f4323fd1ac
12 changed files with 999 additions and 255 deletions

View file

@ -75,6 +75,12 @@
stroke: var(--button-primary-foreground-color-active); stroke: var(--button-primary-foreground-color-active);
} }
} }
&:global(.disabled) {
background-color: var(--button-background-color-disabled);
border: $s-1 solid var(--button-border-color-disabled);
color: var(--button-foreground-color-disabled);
cursor: unset;
}
} }
.button-secondary { .button-secondary {
@ -118,6 +124,12 @@
stroke: var(--button-secondary-foreground-color-active); stroke: var(--button-secondary-foreground-color-active);
} }
} }
&:global(.disabled) {
background-color: var(--button-background-color-disabled);
border: $s-1 solid var(--button-border-color-disabled);
color: var(--button-foreground-color-disabled);
cursor: unset;
}
} }
.button-tertiary { .button-tertiary {
@ -161,6 +173,12 @@
stroke: var(--button-tertiary-foreground-color-active); stroke: var(--button-tertiary-foreground-color-active);
} }
} }
&:global(.disabled) {
background-color: var(--button-background-color-disabled);
border: $s-1 solid var(--button-border-color-disabled);
color: var(--button-foreground-color-disabled);
cursor: unset;
}
} }
.button-radio { .button-radio {
@ -375,6 +393,26 @@
} }
} }
.checkbox-icon {
@include flexCenter;
width: $s-16;
height: $s-16;
min-width: $s-16;
min-height: $s-16;
border-radius: $br-6;
background-color: var(--input-background-color);
svg {
display: none;
}
&:global(.checked) {
background-color: var(--input-border-color-active);
svg {
@extend .button-icon-small;
stroke: var(--input-details-color);
}
}
}
.input-checkbox { .input-checkbox {
display: flex; display: flex;
align-items: center; align-items: center;
@ -385,20 +423,7 @@
gap: $s-6; gap: $s-6;
cursor: pointer; cursor: pointer;
span { span {
@include flexCenter; @extend .checkbox-icon;
width: $s-16;
height: $s-16;
min-width: $s-16;
min-height: $s-16;
border-radius: $br-6;
background-color: var(--input-background-color);
&:global(.checked) {
background-color: var(--input-border-color-active);
svg {
@extend .button-icon-small;
stroke: var(--input-details-color);
}
}
} }
input { input {
margin: 0; margin: 0;
@ -505,6 +530,28 @@
grid-template-columns: $s-92 1fr; grid-template-columns: $s-92 1fr;
} }
.comment-bubbles {
@include titleTipography;
@include flexCenter;
height: $s-32;
width: $s-32;
border-radius: $br-circle;
background-color: var(--comment-bullet-background-color-rest);
border: $s-1 solid var(--comment-bullet-border-color-rest);
color: var(--comment-bullet-foreground-color-rest);
}
.resolved-comment-bubble {
background-color: var(--comment-bullet-background-color-resolved);
border: $s-1 solid var(--comment-bullet-border-color-resolved);
color: var(--comment-bullet-foreground-color-resolved);
}
.unread-comment-bubble {
background-color: var(--comment-bullet-background-color-unread);
border: $s-1 solid var(--comment-bullet-border-color-unread);
color: var(--comment-bullet-foreground-color-unread);
}
// SELECTS AND DROPDOWNS // SELECTS AND DROPDOWNS
.menu-dropdown { .menu-dropdown {
@include menuShadow; @include menuShadow;

View file

@ -20,7 +20,9 @@
--button-background-focus: var(--color-background-secondary); --button-background-focus: var(--color-background-secondary);
--button-foreground-focus: var(--color-foreground-primary); --button-foreground-focus: var(--color-foreground-primary);
--button-border-focus: var(--color-accent-primary); --button-border-focus: var(--color-accent-primary);
--button-foreground-color-disabled: var(--color-background-quaternary); --button-foreground-color-disabled: var(--color-foreground-secondary);
--button-background-color-disabled: var(--color-background-primary);
--button-border-color-disabled: var(--color-background-quaternary);
--button-primary-background-color-rest: var(--color-accent-primary); --button-primary-background-color-rest: var(--color-accent-primary);
--button-primary-border-color-rest: var(--color-accent-primary); --button-primary-border-color-rest: var(--color-accent-primary);
@ -228,4 +230,18 @@
--colorpicker-details-color: var(--color-background-quaternary); --colorpicker-details-color: var(--color-background-quaternary);
--colorpicker-details-color-selected: var(--color-accent-primary); --colorpicker-details-color-selected: var(--color-accent-primary);
--colorpicker-handlers-color: var(--color-foreground-primary); --colorpicker-handlers-color: var(--color-foreground-primary);
// COMMENTS
--comment-title-color: var(--color-foreground-primary);
--comment-subtitle-color: var(--color-foreground-secondary);
--comment-bullet-background-color-rest: var(--color-background-quaternary);
--comment-bullet-foreground-color-rest: var(--color-foreground-primary);
--comment-bullet-border-color-rest: var(--color-background-secondary);
--comment-bullet-background-color-unread: var(--color-accent-primary);
--comment-bullet-foreground-color-unread: var(--color-background-primary);
--comment-bullet-border-color-unread: var(--color-background-secondary);
--comment-bullet-background-color-resolved: var(--color-background-primary);
--comment-bullet-foreground-color-resolved: var(--color-foreground-secondary);
--comment-bullet-border-color-resolved: var(--color-background-quaternary);
--comment-modal-background-color: var(--color-background-primary);
} }

View file

@ -5,6 +5,7 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.main.ui.comments (ns app.main.ui.comments
(:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
@ -17,6 +18,7 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.main.ui.context :as ctx]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@ -69,8 +71,6 @@
;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
(.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))] (.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))]
(mf/use-layout-effect (mf/use-layout-effect
nil nil
(fn [] (fn []
@ -90,9 +90,13 @@
(mf/defc reply-form (mf/defc reply-form
[{:keys [thread] :as props}] [{:keys [thread] :as props}]
(let [show-buttons? (mf/use-state false) (let [new-css-system (mf/use-ctx ctx/new-css-system)
show-buttons? (mf/use-state false)
content (mf/use-state "") content (mf/use-state "")
disabled? (or (fm/all-spaces? @content)
(str/empty-or-nil? @content))
on-focus on-focus
(mf/use-fn (mf/use-fn
#(reset! show-buttons? true)) #(reset! show-buttons? true))
@ -116,29 +120,52 @@
(fn [] (fn []
(st/emit! (dcm/add-comment thread @content)) (st/emit! (dcm/add-comment thread @content))
(on-cancel)))] (on-cancel)))]
(if new-css-system
[:div {:class (stl/css :reply-form)}
[:& resizing-textarea {:value @content
:placeholder "Reply"
:on-blur on-blur
:on-focus on-focus
:select-on-focus? false
:on-change on-change}]
(when (or @show-buttons? (seq @content))
[:div {:class (stl/css :buttons-wrapper)}
[:input.btn-secondary
{:type "button"
:class (stl/css :cancel-btn)
:value "Cancel"
:on-click on-cancel}]
[:input
{:type "button"
:class (stl/css-case :post-btn true
:global/disabled disabled?)
:value "Post"
:on-click on-submit
:disabled disabled?}]])]
[:div.reply-form
[:& resizing-textarea {:value @content [:div.reply-form
:placeholder "Reply" [:& resizing-textarea {:value @content
:on-blur on-blur :placeholder "Reply"
:on-focus on-focus :on-blur on-blur
:on-change on-change}] :on-focus on-focus
(when (or @show-buttons? (seq @content)) :on-change on-change}]
[:div.buttons (when (or @show-buttons? (seq @content))
[:input.btn-primary [:div.buttons
{:type "button" [:input.btn-primary
:value "Post" {:type "button"
:on-click on-submit :value "Post"
:disabled (or (fm/all-spaces? @content) :on-click on-submit
(str/empty-or-nil? @content))}] :disabled disabled?}]
[:input.btn-secondary [:input.btn-secondary
{:type "button" {:type "button"
:value "Cancel" :value "Cancel"
:on-click on-cancel}]])])) :on-click on-cancel}]])])))
(mf/defc draft-thread (mf/defc draft-thread
[{:keys [draft zoom on-cancel on-submit position-modifier]}] [{:keys [draft zoom on-cancel on-submit position-modifier]}]
(let [position (cond-> (:position draft) (let [new-css-system (mf/use-ctx ctx/new-css-system)
position (cond-> (:position draft)
(some? position-modifier) (some? position-modifier)
(gpt/transform position-modifier)) (gpt/transform position-modifier))
content (:content draft) content (:content draft)
@ -146,6 +173,9 @@
pos-x (* (:x position) zoom) pos-x (* (:x position) zoom)
pos-y (* (:y position) zoom) pos-y (* (:y position) zoom)
disabled? (or (fm/all-spaces? content)
(str/empty-or-nil? content))
on-esc on-esc
(mf/use-fn (mf/use-fn
(mf/deps draft) (mf/deps draft)
@ -165,38 +195,70 @@
(mf/use-fn (mf/use-fn
(mf/deps draft) (mf/deps draft)
(partial on-submit draft))] (partial on-submit draft))]
(if new-css-system
[:*
[:div
{:class (stl/css :floating-thread-bubble)
:style {:top (str pos-y "px")
:left (str pos-x "px")}
:on-click dom/stop-propagation}
"?"]
[:div {:class (stl/css :thread-content)
:style {:top (str (- pos-y 24) "px")
:left (str (+ pos-x 28) "px")}
:on-click dom/stop-propagation}
[:div {:class (stl/css :reply-form)}
[:& resizing-textarea {:placeholder (tr "labels.write-new-comment")
:value (or content "")
:autofocus true
:select-on-focus? false
:on-esc on-esc
:on-change on-change}]
[:div {:class (stl/css :buttons-wrapper)}
[:* [:input {:on-click on-esc
[:div.thread-bubble :class (stl/css :cancel-btn)
{:style {:top (str pos-y "px") :type "button"
:left (str pos-x "px")} :value "Cancel"}]
:on-click dom/stop-propagation}
[:span "?"]] [:input {:on-click on-submit
[:div.thread-content :type "button"
{:style {:top (str (- pos-y 14) "px") :value "Post"
:left (str (+ pos-x 14) "px")} :class (stl/css-case :post-btn true
:on-click dom/stop-propagation} :global/disabled disabled?)
[:div.reply-form :disabled disabled?}]]]]]
[:& resizing-textarea {:placeholder (tr "labels.write-new-comment")
:value (or content "") [:*
:autofocus true [:div.thread-bubble
:on-esc on-esc {:style {:top (str pos-y "px")
:on-change on-change}] :left (str pos-x "px")}
[:div.buttons :on-click dom/stop-propagation}
[:input.btn-primary [:span "?"]]
{:on-click on-submit [:div.thread-content
:type "button" {:style {:top (str (- pos-y 14) "px")
:value "Post" :left (str (+ pos-x 14) "px")}
:disabled (or (fm/all-spaces? content) :on-click dom/stop-propagation}
(str/empty-or-nil? content))}] [:div.reply-form
[:input.btn-secondary [:& resizing-textarea {:placeholder (tr "labels.write-new-comment")
{:on-click on-esc :value (or content "")
:type "button" :autofocus true
:value "Cancel"}]]]]])) :on-esc on-esc
:on-change on-change}]
[:div.buttons
[:input.btn-primary
{:on-click on-submit
:type "button"
:value "Post"
:disabled disabled?}]
[:input.btn-secondary
{:on-click on-esc
:type "button"
:value "Cancel"}]]]]])))
(mf/defc edit-form (mf/defc edit-form
[{:keys [content on-submit on-cancel] :as props}] [{:keys [content on-submit on-cancel] :as props}]
(let [content (mf/use-state content) (let [new-css-system (mf/use-ctx ctx/new-css-system)
content (mf/use-state content)
on-change on-change
(mf/use-fn (mf/use-fn
@ -205,34 +267,56 @@
on-submit* on-submit*
(mf/use-fn (mf/use-fn
(mf/deps @content) (mf/deps @content)
(fn [] (on-submit @content)))] (fn [] (on-submit @content)))
disabled? (or (fm/all-spaces? @content)
(str/empty-or-nil? @content))]
(if new-css-system
[:div {:class (stl/css :edit-form)}
[:& resizing-textarea {:value @content
:autofocus true
:select-on-focus true
:select-on-focus? false
:on-change on-change}]
[:div {:class (stl/css :buttons-wrapper)}
[:input {:type "button"
:value "Cancel"
:class (stl/css :cancel-btn)
:on-click on-cancel}]
[:input {:type "button"
:class (stl/css-case :post-btn true
:global/disabled disabled?)
:value "Post"
:on-click on-submit*
:disabled disabled?}]]]
[:div.reply-form.edit-form [:div.reply-form.edit-form
[:& resizing-textarea {:value @content [:& resizing-textarea {:value @content
:autofocus true :autofocus true
:select-on-focus true :select-on-focus true
:on-change on-change}] :on-change on-change}]
[:div.buttons [:div.buttons
[:input.btn-primary {:type "button" [:input.btn-primary {:type "button"
:value "Post" :value "Post"
:on-click on-submit* :on-click on-submit*
:disabled (or (fm/all-spaces? @content) :disabled disabled?}]
(str/empty-or-nil? @content))}] [:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]]])))
[:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]]]))
(mf/defc comment-item (mf/defc comment-item
[{:keys [comment thread users origin] :as props}] [{:keys [comment thread users origin] :as props}]
(let [owner (get users (:owner-id comment)) (let [new-css-system (mf/use-ctx ctx/new-css-system)
owner (get users (:owner-id comment))
profile (mf/deref refs/profile) profile (mf/deref refs/profile)
options (mf/use-state false) options (mf/use-state false)
edition? (mf/use-state false) edition? (mf/use-state false)
on-show-options on-toggle-options
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(reset! options true))) (swap! options not)))
on-hide-options on-hide-options
(mf/use-fn (mf/use-fn
@ -264,11 +348,11 @@
(mf/use-fn (mf/use-fn
(mf/deps thread) (mf/deps thread)
#(st/emit! (modal/show #(st/emit! (modal/show
{:type :confirm {:type :confirm
:title (tr "modals.delete-comment-thread.title") :title (tr "modals.delete-comment-thread.title")
:message (tr "modals.delete-comment-thread.message") :message (tr "modals.delete-comment-thread.message")
:accept-label (tr "modals.delete-comment-thread.accept") :accept-label (tr "modals.delete-comment-thread.accept")
:on-accept delete-thread}))) :on-accept delete-thread})))
on-submit on-submit
(mf/use-fn (mf/use-fn
@ -287,39 +371,82 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dcm/update-comment-thread (update thread :is-resolved not)))))] (st/emit! (dcm/update-comment-thread (update thread :is-resolved not)))))]
[:div.comment-container (if new-css-system
[:div.comment [:div {:class (stl/css :comment-container)}
[:div.author [:div {:class (stl/css :comment)}
[:div.avatar [:div {:class (stl/css :author)}
[:img {:src (cfg/resolve-profile-photo-url owner)}]] [:div {:class (stl/css :avatar)}
[:div.name [:img {:src (cfg/resolve-profile-photo-url owner)}]]
[:div.fullname (:fullname owner)] [:div {:class (stl/css :name)}
[:div.timeago (dt/timeago (:modified-at comment))]] [:div {:class (stl/css :fullname)} (:fullname owner)]
[:div {:class (stl/css :timeago)} (dt/timeago (:modified-at comment))]]
(when (some? thread) (when (some? thread)
[:div.options-resolve {:on-click toggle-resolved} [:div {:class (stl/css :options-resolve-wrapper)
(if (:is-resolved thread) :on-click toggle-resolved}
[:span i/checkbox-checked] [:span {:class (stl/css-case :options-resolve true
[:span i/checkbox-unchecked])]) :global/checked (:is-resolved thread))} i/tick-refactor]])
(when (= (:id profile) (:id owner)) (when (= (:id profile) (:id owner))
[:div.options [:div {:class (stl/css :options)
[:div.options-icon {:on-click on-show-options} i/actions]])] :on-click on-toggle-options}
i/menu-refactor])]
[:div.content [:div {:class (stl/css :content)}
(if @edition? (if @edition?
[:& edit-form {:content (:content comment) [:& edit-form {:content (:content comment)
:on-submit on-submit :on-submit on-submit
:on-cancel on-cancel}] :on-cancel on-cancel}]
[:span.text (:content comment)])]] [:span {:class (stl/css :text)} (:content comment)])]]
[:& dropdown {:show @options [:& dropdown {:show @options
:on-close on-hide-options} :on-close on-hide-options}
[:ul.dropdown.comment-options-dropdown [:ul {:class (stl/css :comment-options-dropdown)}
[:li {:on-click on-edit-clicked} (tr "labels.edit")] [:li {:class (stl/css :context-menu-option)
(if thread :on-click on-edit-clicked}
[:li {:on-click on-delete-thread} (tr "labels.delete-comment-thread")] (tr "labels.edit")]
[:li {:on-click on-delete-comment} (tr "labels.delete-comment")])]]])) (if thread
[:li {:class (stl/css :context-menu-option)
:on-click on-delete-thread}
(tr "labels.delete-comment-thread")]
[:li {:class (stl/css :context-menu-option)
:on-click on-delete-comment}
(tr "labels.delete-comment")])]]]
[:div.comment-container
[:div.comment
[:div.author
[:div.avatar
[:img {:src (cfg/resolve-profile-photo-url owner)}]]
[:div.name
[:div.fullname (:fullname owner)]
[:div.timeago (dt/timeago (:modified-at comment))]]
(when (some? thread)
[:div.options-resolve {:on-click toggle-resolved}
(if (:is-resolved thread)
[:span i/checkbox-checked]
[:span i/checkbox-unchecked])])
(when (= (:id profile) (:id owner))
[:div.options
[:div.options-icon {:on-click on-toggle-options} i/actions]])]
[:div.content
(if @edition?
[:& edit-form {:content (:content comment)
:on-submit on-submit
:on-cancel on-cancel}]
[:span.text (:content comment)])]]
[:& dropdown {:show @options
:on-close on-hide-options}
[:ul.dropdown.comment-options-dropdown
[:li {:on-click on-edit-clicked} (tr "labels.edit")]
(if thread
[:li {:on-click on-delete-thread} (tr "labels.delete-comment-thread")]
[:li {:on-click on-delete-comment} (tr "labels.delete-comment")])]]])))
(defn make-comments-ref (defn make-comments-ref
[thread-id] [thread-id]
@ -328,7 +455,8 @@
(mf/defc thread-comments (mf/defc thread-comments
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [thread zoom users origin position-modifier]}] [{:keys [thread zoom users origin position-modifier]}]
(let [ref (mf/use-ref) (let [new-css-system (mf/use-ctx ctx/new-css-system)
ref (mf/use-ref)
thread-id (:id thread) thread-id (:id thread)
@ -338,8 +466,13 @@
(some? position-modifier) (some? position-modifier)
(gpt/transform position-modifier)) (gpt/transform position-modifier))
pos-x (+ (* (:x pos) zoom) 14) pos-x (if new-css-system
pos-y (- (* (:y pos) zoom) 14) (+ (* (:x pos) zoom) 24)
(+ (* (:x pos) zoom) 14))
pos-y (if new-css-system
(- (* (:y pos) zoom) 28)
(- (* (:y pos) zoom) 14))
comments-ref (mf/with-memo [thread-id] comments-ref (mf/with-memo [thread-id]
(make-comments-ref thread-id)) (make-comments-ref thread-id))
@ -360,26 +493,46 @@
(mf/with-layout-effect [thread-pos comments-map] (mf/with-layout-effect [thread-pos comments-map]
(when-let [node (mf/ref-val ref)] (when-let [node (mf/ref-val ref)]
(dom/scroll-into-view-if-needed! node))) (dom/scroll-into-view-if-needed! node)))
(if new-css-system
(when (some? comment)
[:div {:class (stl/css :thread-content)
:style {:top (str pos-y "px")
:left (str pos-x "px")}
:on-click dom/stop-propagation}
(when (some? comment) [:div {:class (stl/css :comments)}
[:div.thread-content [:& comment-item {:comment comment
{:style {:top (str pos-y "px") :users users
:left (str pos-x "px")} :thread thread
:on-click dom/stop-propagation} :origin origin}]
(for [item (rest comments)]
[:* {:key (dm/str (:id item))}
[:& comment-item {:comment item
:users users
:origin origin}]])
[:div {:ref ref}]]
[:& reply-form {:thread thread}]])
[:div.comments
[:& comment-item {:comment comment (when (some? comment)
:users users [:div.thread-content
:thread thread {:style {:top (str pos-y "px")
:origin origin}] :left (str pos-x "px")}
(for [item (rest comments)] :on-click dom/stop-propagation}
[:* {:key (dm/str (:id item))}
[:hr] [:div.comments
[:& comment-item {:comment item [:& comment-item {:comment comment
:users users :users users
:origin origin}]]) :thread thread
[:div {:ref ref}]] :origin origin}]
[:& reply-form {:thread thread}]]))) (for [item (rest comments)]
[:* {:key (dm/str (:id item))}
[:hr]
[:& comment-item {:comment item
:users users
:origin origin}]])
[:div {:ref ref}]]
[:& reply-form {:thread thread}]]))))
(defn use-buble (defn use-buble
[zoom {:keys [position frame-id]}] [zoom {:keys [position frame-id]}]
@ -438,7 +591,8 @@
(mf/defc thread-bubble (mf/defc thread-bubble
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [thread zoom open? on-click origin position-modifier]}] [{:keys [thread zoom open? on-click origin position-modifier]}]
(let [pos (cond-> (:position thread) (let [new-css-system (mf/use-ctx ctx/new-css-system)
pos (cond-> (:position thread)
(some? position-modifier) (some? position-modifier)
(gpt/transform position-modifier)) (gpt/transform position-modifier))
@ -493,23 +647,37 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(when (= origin :viewer) (when (= origin :viewer)
(on-click thread))))] (on-click thread))))]
(if new-css-system
[:div {:style {:top (str pos-y "px")
:left (str pos-x "px")}
:on-pointer-down on-pointer-down*
:on-pointer-up on-pointer-up*
:on-pointer-move on-pointer-move*
:on-click on-click*
:on-lost-pointer-capture on-lost-pointer-capture
:class (stl/css-case
:floating-thread-bubble true
:resolved (:is-resolved thread)
:unread (pos? (:count-unread-comments thread)))}
[:span (:seqn thread)]]
[:div.thread-bubble [:div.thread-bubble
{:style {:top (str pos-y "px") {:style {:top (str pos-y "px")
:left (str pos-x "px")} :left (str pos-x "px")}
:on-pointer-down on-pointer-down* :on-pointer-down on-pointer-down*
:on-pointer-up on-pointer-up* :on-pointer-up on-pointer-up*
:on-pointer-move on-pointer-move* :on-pointer-move on-pointer-move*
:on-click on-click* :on-click on-click*
:on-lost-pointer-capture on-lost-pointer-capture :on-lost-pointer-capture on-lost-pointer-capture
:class (dom/classnames :class (dom/classnames
:resolved (:is-resolved thread) :resolved (:is-resolved thread)
:unread (pos? (:count-unread-comments thread)))} :unread (pos? (:count-unread-comments thread)))}
[:span (:seqn thread)]])) [:span (:seqn thread)]])))
(mf/defc comment-thread (mf/defc comment-thread
[{:keys [item users on-click]}] [{:keys [item users on-click]}]
(let [owner (get users (:owner-id item)) (let [new-css-system (mf/use-ctx ctx/new-css-system)
owner (get users (:owner-id item))
on-click* on-click*
(mf/use-fn (mf/use-fn
(mf/deps item) (mf/deps item)
@ -519,48 +687,99 @@
(when (fn? on-click) (when (fn? on-click)
(on-click item))))] (on-click item))))]
[:div.comment {:on-click on-click*} (if new-css-system
[:div.author [:div {:class (stl/css :comment)
[:div.thread-bubble :on-click on-click*}
{:class (dom/classnames [:div {:class (stl/css :author)}
:resolved (:is-resolved item) [:div {:class (stl/css-case :thread-bubble true
:unread (pos? (:count-unread-comments item)))} :resolved (:is-resolved item)
(:seqn item)] :unread (pos? (:count-unread-comments item)))}
[:div.avatar (:seqn item)]
[:img {:src (cfg/resolve-profile-photo-url owner)}]] [:div {:class (stl/css :avatar)}
[:div.name [:img {:src (cfg/resolve-profile-photo-url owner)}]]
[:div.fullname (:fullname owner) ", "] [:div {:class (stl/css :name)}
[:div.timeago (dt/timeago (:modified-at item))]]] [:div {:class (stl/css :fullname)} (:fullname owner)]
[:div.content [:div {:class (stl/css :timeago)} (dt/timeago (:modified-at item))]]]
[:span.text (:content item)]] [:div {:class (stl/css :content)}
[:div.content.replies (:content item)]
(let [unread (:count-unread-comments item ::none) [:div {:class (stl/css :replies)}
total (:count-comments item 1)] (let [unread (:count-unread-comments item ::none)
[:* total (:count-comments item 1)]
(when (> total 1) [:*
(if (= total 2) (when (> total 1)
[:span.total-replies "1 reply"] (if (= total 2)
[:span.total-replies (str (dec total) " replies")])) [:span {:class (stl/css :total-replies)} "1 reply"]
[:span {:class (stl/css :total-replies)} (str (dec total) " replies")]))
(when (and (> total 1) (> unread 0)) (when (and (> total 1) (> unread 0))
(if (= unread 1) (if (= unread 1)
[:span.new-replies "1 new reply"] [:span {:class (stl/css :new-replies)} "1 new reply"]
[:span.new-replies (str unread " new replies")]))])]])) [:span {:class (stl/css :new-replies)} (str unread " new replies")]))])]]
[:div.comment {:on-click on-click*}
[:div.author
[:div.thread-bubble
{:class (dom/classnames
:resolved (:is-resolved item)
:unread (pos? (:count-unread-comments item)))}
(:seqn item)]
[:div.avatar
[:img {:src (cfg/resolve-profile-photo-url owner)}]]
[:div.name
[:div.fullname (:fullname owner) ", "]
[:div.timeago (dt/timeago (:modified-at item))]]]
[:div.content
[:span.text (:content item)]]
[:div.content.replies
(let [unread (:count-unread-comments item ::none)
total (:count-comments item 1)]
[:*
(when (> total 1)
(if (= total 2)
[:span.total-replies "1 reply"]
[:span.total-replies (str (dec total) " replies")]))
(when (and (> total 1) (> unread 0))
(if (= unread 1)
[:span.new-replies "1 new reply"]
[:span.new-replies (str unread " new replies")]))])]])))
(mf/defc comment-thread-group (mf/defc comment-thread-group
[{:keys [group users on-thread-click]}] [{:keys [group users on-thread-click]}]
[:div.thread-group (let [new-css-system (mf/use-ctx ctx/new-css-system)]
(if (:file-name group) (if new-css-system
[:div.section-title [:div {:class (stl/css :thread-group)}
[:span.label.filename (:file-name group) ", "] (if (:file-name group)
[:span.label (:page-name group)]] [:div {:class (stl/css :section-title)}
[:div.section-title [:span {:class (stl/css :file-name)} (:file-name group) ", "]
[:span.icon i/file-html] [:span {:class (stl/css :page-name)} (:page-name group)]]
[:span.label (:page-name group)]])
[:div.threads [:div {:class (stl/css :section-title)}
(for [item (:items group)] [:span {:class (stl/css :icon)} i/document-refactor]
[:& comment-thread [:span {:class (stl/css :page-name)} (:page-name group)]])
{:item item
:on-click on-thread-click [:div {:class (stl/css :threads)}
:users users (for [item (:items group)]
:key (:id item)}])]]) [:& comment-thread
{:item item
:on-click on-thread-click
:users users
:key (:id item)}])]]
[:div.thread-group
(if (:file-name group)
[:div.section-title
[:span.label.filename (:file-name group) ", "]
[:span.label (:page-name group)]]
[:div.section-title
[:span.icon i/file-html]
[:span.label (:page-name group)]])
[:div.threads
(for [item (:items group)]
[:& comment-thread
{:item item
:on-click on-thread-click
:users users
:key (:id item)}])]])))

View file

@ -0,0 +1,232 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@import "refactor/common-refactor.scss";
// Comment-thread-group
.thread-group {
padding: 0 $s-12;
.section-title {
@include titleTipography;
height: $s-32;
display: flex;
align-items: center;
margin-bottom: $s-8;
.file-name {
color: var(--comment-subtitle-color);
}
.page-name {
color: var(--comment-subtitle-color);
}
.icon {
display: flex;
align-items: center;
padding: 0 $s-6 0 $s-4;
width: $s-24;
height: $s-32;
margin-left: $s-6;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
}
.threads {
display: flex;
flex-direction: column;
gap: $s-24;
}
}
// Comment-thread
.comment {
@include titleTipography;
display: flex;
flex-direction: column;
gap: $s-12;
.author {
display: flex;
gap: $s-8;
.thread-bubble {
@extend .comment-bubbles;
&.resolved {
@extend .resolved-comment-bubble;
}
&.unread {
@extend .unread-comment-bubble;
}
}
.avatar {
height: $s-32;
width: $s-32;
border-radius: $br-circle;
img {
border-radius: $br-circle;
}
}
.name {
flex-grow: 1;
.fullname {
@include textEllipsis;
color: var(--comment-title-color);
}
.timeago {
@include textEllipsis;
color: var(--comment-subtitle-color);
}
}
}
.content {
@include titleTipography;
color: var(--color-foreground-primary);
}
.replies {
display: flex;
gap: $s-8;
.total-replies {
color: var(--color-foreground-secondary);
}
.new-replies {
color: var(--color-accent-primary);
}
}
}
// Thread-bubble
.floating-thread-bubble {
@extend .comment-bubbles;
position: absolute;
cursor: pointer;
pointer-events: auto;
transform: translate(calc(-1 * $s-16), calc(-1 * $s-16));
&.resolved {
@extend .resolved-comment-bubble;
}
&.unread {
@extend .unread-comment-bubble;
}
}
// thread-content
.thread-content {
position: absolute;
pointer-events: auto;
user-select: text;
width: $s-284;
padding: $s-12;
border-radius: $br-8;
background-color: var(--comment-modal-background-color);
.comments {
display: flex;
flex-direction: column;
gap: $s-24;
}
}
// comment-item
.comment-container {
position: relative;
.comment {
@include titleTipography;
.author {
display: flex;
gap: $s-8;
.avatar {
height: $s-32;
width: $s-32;
border-radius: $br-circle;
img {
border-radius: $br-circle;
}
}
.name {
flex-grow: 1;
.fullname {
@include textEllipsis;
color: var(--comment-title-color);
}
.timeago {
@include textEllipsis;
color: var(--comment-subtitle-color);
}
}
.options-resolve-wrapper {
@include flexCenter;
width: $s-16;
height: $s-32;
.options-resolve {
@extend .checkbox-icon;
cursor: pointer;
}
}
.options {
@extend .button-tertiary;
height: $s-32;
width: $s-28;
svg {
@extend .button-icon;
}
}
}
.content {
position: relative;
.text {
@include titleTipography;
}
}
}
.comment-options-dropdown {
@extend .dropdown-wrapper;
position: absolute;
width: $s-120;
right: 0;
left: unset;
.context-menu-option {
@extend .dropdown-element-base;
}
}
}
// edit-form & reply-form
.edit-form,
.reply-form {
textarea {
@extend .input-element;
line-height: 1.45;
height: 100%;
width: 100%;
max-width: $s-260;
min-width: $s-260;
margin-bottom: $s-8;
padding: $s-8;
color: var(--input-foreground-color-active);
&:focus {
border: $s-1 solid var(--input-border-color-active);
outline: none;
}
}
.buttons-wrapper {
display: flex;
justify-content: flex-end;
gap: $s-4;
.post-btn {
@extend .button-primary;
height: $s-32;
width: $s-92;
margin-bottom: 0;
}
.cancel-btn {
@extend .button-secondary;
height: $s-32;
width: $s-92;
margin-bottom: 0;
}
}
}

View file

@ -44,19 +44,19 @@
&.disabled { &.disabled {
cursor: default; cursor: default;
svg { svg {
stroke: var(--button-foreground-color-disabled); stroke: var(--button-background-color-disabled);
} }
.title-name { .title-name {
color: var(--button-foreground-color-disabled); color: var(--button-background-color-disabled);
} }
&:hover { &:hover {
border: none; border: none;
background-color: transparent; background-color: transparent;
svg { svg {
stroke: var(--button-foreground-color-disabled); stroke: var(--button-background-color-disabled);
} }
.title-name { .title-name {
color: var(--button-foreground-color-disabled); color: var(--button-background-color-disabled);
} }
} }
} }

View file

@ -45,7 +45,7 @@
} }
&:disabled { &:disabled {
svg { svg {
stroke: var(--button-foreground-color-disabled); stroke: var(--button-background-color-disabled);
} }
&::after { &::after {
background-image: none; background-image: none;

View file

@ -5,6 +5,7 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.main.ui.workspace.comments (ns app.main.ui.workspace.comments
(:require-macros [app.main.style :as stl])
(:require (:require
[app.main.data.comments :as dcm] [app.main.data.comments :as dcm]
[app.main.data.events :as ev] [app.main.data.events :as ev]
@ -27,54 +28,104 @@
(mf/defc sidebar-options (mf/defc sidebar-options
[] []
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local) (let [new-css-system (mf/use-ctx ctx/new-css-system)
{cmode :mode cshow :show} (mf/deref refs/comments-local)
update-mode update-mode
(mf/use-fn (mf/use-fn
(fn [mode] (fn [event]
(st/emit! (dcm/update-filters {:mode mode})))) (let [mode (-> (dom/get-current-target event)
(dom/get-data "value")
(keyword))]
(st/emit! (dcm/update-filters {:mode mode})))))
update-show update-show
(mf/use-fn (mf/use-fn
(fn [mode] (mf/deps cshow)
(st/emit! (dcm/update-filters {:show mode}))))] (fn []
(let [mode (if (= :pending cshow) :all :pending)]
(st/emit! (dcm/update-filters {:show mode})))))]
[:ul.dropdown.with-check (if new-css-system
[:li {:class (dom/classnames :selected (or (= :all cmode) (nil? cmode))) [:ul {:class (stl/css :comment-mode-dropdown)}
:on-click #(update-mode :all)} [:li {:class (stl/css-case :dropdown-item true
[:span.icon i/tick] :selected (or (= :all cmode) (nil? cmode)))
[:span.label (tr "labels.show-all-comments")]] :data-value "all"
:on-click update-mode}
[:li {:class (dom/classnames :selected (= :yours cmode)) [:span {:class (stl/css :label)} (tr "labels.show-all-comments")]
:on-click #(update-mode :yours)} [:span {:class (stl/css :icon)} i/tick-refactor]]
[:span.icon i/tick] [:li {:class (stl/css-case :dropdown-item true
[:span.label (tr "labels.show-your-comments")]] :selected (= :yours cmode))
:data-value "yours"
:on-click update-mode}
[:span {:class (stl/css :label)} (tr "labels.show-your-comments")]
[:span {:class (stl/css :icon)} i/tick-refactor]]
[:li {:class (stl/css :separator)}]
[:li {:class (stl/css-case :dropdown-item true
:selected (= :pending cshow))
:on-click update-show}
[:span {:class (stl/css :label)} (tr "labels.hide-resolved-comments")]
[:span {:class (stl/css :icon)} i/tick-refactor]]]
[:hr]
[:li {:class (dom/classnames :selected (= :pending cshow)) [:ul.dropdown.with-check
:on-click #(update-show (if (= :pending cshow) :all :pending))} [:li {:class (dom/classnames :selected (or (= :all cmode) (nil? cmode)))
[:span.icon i/tick] :data-value "all"
[:span.label (tr "labels.hide-resolved-comments")]]])) :on-click update-mode}
[:span.icon i/tick]
[:span.label (tr "labels.show-all-comments")]]
[:li {:class (dom/classnames :selected (= :yours cmode))
:data-value "yours"
:on-click update-mode}
[:span.icon i/tick]
[:span.label (tr "labels.show-your-comments")]]
[:hr]
[:li {:class (dom/classnames :selected (= :pending cshow))
:on-click update-show}
[:span.icon i/tick]
[:span.label (tr "labels.hide-resolved-comments")]]])
))
(mf/defc comments-sidebar (mf/defc comments-sidebar
[{:keys [users threads page-id]}] [{:keys [users threads page-id]}]
(let [threads-map (mf/deref refs/threads-ref) (let [new-css-system (mf/use-ctx ctx/new-css-system)
threads-map (mf/deref refs/threads-ref)
profile (mf/deref refs/profile) profile (mf/deref refs/profile)
users-refs (mf/deref refs/current-file-comments-users) users-refs (mf/deref refs/current-file-comments-users)
users (or users users-refs) users (or users users-refs)
local (mf/deref refs/comments-local) local (mf/deref refs/comments-local)
options? (mf/use-state false)
state* (mf/use-state false)
options? (deref state*)
threads (if (nil? threads) threads (if (nil? threads)
(->> (vals threads-map) (->> (vals threads-map)
(sort-by :modified-at) (sort-by :modified-at)
(reverse) (reverse)
(dcm/apply-filters local profile)) (dcm/apply-filters local profile))
threads) threads)
tgroups (->> threads
(dcm/group-threads-by-page)) close-section
(mf/use-fn
(mf/deps)
#(st/emit! :interrupt (dw/deselect-all true)))
tgroups (->> threads
(dcm/group-threads-by-page))
page-id (or page-id (mf/use-ctx ctx/current-page-id)) page-id (or page-id (mf/use-ctx ctx/current-page-id))
toggle-mode-selector
(mf/use-fn
(mf/deps)
(fn [event]
(dom/stop-propagation event)
(swap! state* not)))
on-thread-click on-thread-click
(mf/use-fn (mf/use-fn
(mf/deps page-id) (mf/deps page-id)
@ -88,38 +139,76 @@
(dwcm/center-to-comment-thread thread) (dwcm/center-to-comment-thread thread)
(-> (dcm/open-thread thread) (-> (dcm/open-thread thread)
(with-meta {::ev/origin "workspace"})))))))] (with-meta {::ev/origin "workspace"})))))))]
(if new-css-system
[:div {:class (stl/css :comments-section)}
[:div {:class (stl/css :comments-section-title)}
[:span (tr "labels.comments")]
[:button {:class (stl/css :close-button)
:on-click close-section}
i/close-refactor]]
[:div.comments-section.comment-threads-section (when (seq tgroups)
[:div.workspace-comment-threads-sidebar-header [:button {:class (stl/css :mode-dropdown-wrapper)
[:div.label (tr "labels.comments")] :on-click toggle-mode-selector}
[:div.options {:on-click
(fn [event]
(dom/stop-propagation event)
(reset! options? true))}
[:div.label (case (:mode local)
(nil :all) (tr "labels.all")
:yours (tr "labels.only-yours"))]
[:div.icon i/arrow-down]]
[:& dropdown {:show @options? [:span {:class (stl/css :mode-label)} (case (:mode local)
:on-close #(reset! options? false)} (nil :all) (tr "labels.show-all-comments")
[:& sidebar-options {:local local}]]] :yours (tr "labels.show-your-comments"))]
[:div {:class (stl/css :icon)} i/arrow-refactor]]
(if (seq tgroups) [:& dropdown {:show options?
[:div.thread-groups :on-close #(reset! state* false)}
[:& cmt/comment-thread-group [:& sidebar-options {:local local}]])
{:group (first tgroups)
:on-thread-click on-thread-click [:div {:class (stl/css :comments-section-content)}
:users users}]
(for [tgroup (rest tgroups)] (if (seq tgroups)
[:* [:div {:class (stl/css :thread-groups)}
[:hr]
[:& cmt/comment-thread-group [:& cmt/comment-thread-group
{:group tgroup {:group (first tgroups)
:on-thread-click on-thread-click :on-thread-click on-thread-click
:users users :users users}]
:key (:page-id tgroup)}]])] (for [tgroup (rest tgroups)]
[:& cmt/comment-thread-group
{:group tgroup
:on-thread-click on-thread-click
:users users
:key (:page-id tgroup)}])]
[:div.thread-groups-placeholder [:div {:class (stl/css :thread-group-placeholder)}
i/chat [:span {:class (stl/css :placeholder-icon)} i/comments-refactor]
(tr "labels.no-comments-available")])])) [:span {:class (stl/css :placeholder-label)}
(tr "labels.no-comments-available")]])]]
[:div.comments-section.comment-threads-section
[:div.workspace-comment-threads-sidebar-header
[:div.label (tr "labels.comments")]
[:div.options {:on-click toggle-mode-selector}
[:div.label (case (:mode local)
(nil :all) (tr "labels.all")
:yours (tr "labels.only-yours"))]
[:div.icon i/arrow-down]]
[:& dropdown {:show options?
:on-close #(reset! state* false)}
[:& sidebar-options {:local local}]]]
(if (seq tgroups)
[:div.thread-groups
[:& cmt/comment-thread-group
{:group (first tgroups)
:on-thread-click on-thread-click
:users users}]
(for [tgroup (rest tgroups)]
[:*
[:hr]
[:& cmt/comment-thread-group
{:group tgroup
:on-thread-click on-thread-click
:users users
:key (:page-id tgroup)}]])]
[:div.thread-groups-placeholder
i/chat
(tr "labels.no-comments-available")])])))

View file

@ -0,0 +1,141 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@import "refactor/common-refactor.scss";
.comments-section {
position: relative;
background-color: var(--panel-background-color);
.comments-section-title {
@include flexCenter;
@include tabTitleTipography;
display: flex;
justify-content: space-between;
align-items: center;
height: $s-32;
min-height: $s-32;
margin: $s-8 $s-8 0 $s-8;
border-radius: $br-8;
background-color: var(--panel-title-background-color);
span {
@include flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
.close-button {
@extend .button-tertiary;
height: $s-28;
width: $s-28;
border-radius: $br-6;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
}
.mode-dropdown-wrapper {
@include buttonStyle;
@extend .asset-element;
background-color: var(--color-background-tertiary);
display: flex;
width: $s-256;
height: $s-32;
padding: $s-8;
border-radius: $br-8;
margin: $s-16 auto 0 auto;
cursor: pointer;
position: relative;
.mode-label {
padding-right: 8px;
flex-grow: 1;
display: flex;
justify-content: flex-start;
}
.icon {
@include flexCenter;
padding-right: 8px;
height: $s-24;
width: $s-24;
svg {
@extend .button-icon-small;
transform: rotate(90deg);
stroke: var(--icon-foreground);
}
}
}
.comment-mode-dropdown {
@extend .dropdown-wrapper;
top: $s-80;
left: $s-12;
width: $s-256;
.dropdown-item {
@extend .dropdown-element-base;
justify-content: space-between;
.icon {
@include flexCenter;
height: $s-24;
width: $s-24;
svg {
@extend .button-icon-small;
stroke: transparent;
}
}
.label {
@include titleTipography;
}
&:hover {
.icon svg {
stroke: transparent;
}
}
&.selected {
.label {
color: var(--menu-foreground-color);
}
.icon svg {
stroke: var(--icon-foreground-hover);
}
}
}
.separator {
height: $s-12;
}
}
.comments-section-content {
.thread-groups {
display: flex;
flex-direction: column;
gap: $s-24;
}
.thread-group-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: $s-4;
margin-top: $s-36;
.placeholder-icon {
@include flexCenter;
height: $s-48;
width: $s-48;
border-radius: $br-circle;
background-color: var(--empty-message-background-color);
svg {
@extend .button-icon;
height: $s-28;
width: $s-28;
stroke: var(--empty-message-foreground-color);
}
}
.placeholder-label {
@include titleTipography;
text-align: center;
width: $s-184;
}
}
}
}

View file

@ -17,7 +17,7 @@
position: relative; position: relative;
height: $s-32; height: $s-32;
min-height: $s-32; min-height: $s-32;
margin: $s-4 $s-4 0 $s-4; margin: $s-8 $s-8 0 $s-8;
border-radius: $br-8; border-radius: $br-8;
background-color: var(--panel-title-background-color); background-color: var(--panel-title-background-color);
@ -53,7 +53,7 @@
@include flexCenter; @include flexCenter;
height: $s-48; height: $s-48;
width: $s-48; width: $s-48;
border-radius: 50%; border-radius: $br-circle;
background-color: var(--empty-message-background-color); background-color: var(--empty-message-background-color);
svg { svg {
@extend .button-icon; @extend .button-icon;

View file

@ -28,12 +28,12 @@
&.disabled { &.disabled {
cursor: default; cursor: default;
svg { svg {
stroke: var(--button-foreground-color-disabled); stroke: var(--button-background-color-disabled);
} }
&:hover { &:hover {
background-color: var(--panel-background-color); background-color: var(--panel-background-color);
svg { svg {
stroke: var(--button-foreground-color-disabled); stroke: var(--button-background-color-disabled);
} }
} }
} }

View file

@ -26,12 +26,12 @@
&.disabled { &.disabled {
cursor: default; cursor: default;
svg { svg {
stroke: var(--button-foreground-color-disabled); stroke: var(--button-background-color-disabled);
} }
&:hover { &:hover {
background-color: var(--panel-background-color); background-color: var(--panel-background-color);
svg { svg {
stroke: var(--button-foreground-color-disabled); stroke: var(--button-background-color-disabled);
} }
} }
} }

View file

@ -43,7 +43,7 @@
} }
&:disabled { &:disabled {
svg { svg {
stroke: var(--button-foreground-color-disabled); stroke: var(--button-background-color-disabled);
} }
&::after { &::after {
background-image: none; background-image: none;