diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs index d40734f70d..e13527db00 100644 --- a/frontend/src/app/main/data/tokens.cljs +++ b/frontend/src/app/main/data/tokens.cljs @@ -102,6 +102,17 @@ (rx/of (dch/commit-changes changes))))))) +(defn update-token-theme [token-theme] + (ptk/reify ::update-token-theme + ptk/WatchEvent + (watch [it state _] + (let [prev-token-theme (wtts/get-workspace-token-theme state (:id token-theme)) + changes (-> (pcb/empty-changes it) + (pcb/update-token-theme token-theme prev-token-theme))] + (js/console.log "changes" changes) + (rx/of + (dch/commit-changes changes)))))) + (defn ensure-token-theme-changes [changes state {:keys [id new-set?]}] (let [theme-id (wtts/update-theme-id state) theme (some-> theme-id (wtts/get-workspace-token-theme state))] @@ -155,19 +166,32 @@ (ensure-token-theme-changes state {:id (:id new-token-set) :new-set? true}))] (rx/of + (set-selected-token-set-id (:id new-token-set)) (dch/commit-changes changes))))))) -(defn toggle-token-set [token-set-id] +(defn update-token-set [token-set] + (ptk/reify ::update-token-set + ptk/WatchEvent + (watch [it state _] + (let [prev-token-set (wtts/get-token-set (:id token-set) state) + changes (-> (pcb/empty-changes it) + (pcb/update-token-set token-set prev-token-set))] + (rx/of + (dch/commit-changes changes)))))) + +(defn toggle-token-set [{:keys [token-set-id]}] (ptk/reify ::toggle-token-set ptk/WatchEvent (watch [it state _] - (let [theme (some-> (wtts/update-theme-id state) - (wtts/get-workspace-token-theme state)) + (let [target-theme-id (wtts/get-temp-theme-id state) + active-set-ids (wtts/get-active-set-ids state) + theme (-> (wtts/get-workspace-token-theme target-theme-id state) + (assoc :sets active-set-ids)) changes (-> (pcb/empty-changes it) (pcb/update-token-theme (wtts/toggle-token-set-to-token-theme token-set-id theme) theme) - (pcb/update-active-token-themes #{(wtts/update-theme-id state)} (wtts/get-active-theme-ids state)))] + (pcb/update-active-token-themes #{target-theme-id} (wtts/get-active-theme-ids state)))] (rx/of (dch/commit-changes changes) (wtu/update-workspace-tokens)))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index b1177e8c25..3e6dbf02c3 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -240,9 +240,16 @@ st/state =)) +(defn workspace-token-theme + [id] + (l/derived #(wtts/get-workspace-theme id %) st/state)) + (def workspace-active-theme-ids (l/derived wtts/get-active-theme-ids st/state)) +(def workspace-temp-theme-id + (l/derived wtts/get-temp-theme-id st/state)) + (def workspace-active-set-ids (l/derived wtts/get-active-set-ids st/state)) @@ -252,16 +259,19 @@ (def workspace-ordered-token-themes (l/derived wtts/get-workspace-ordered-themes st/state)) -(def workspace-token-sets +(def workspace-ordered-token-sets (l/derived (fn [data] - (or (wtts/get-workspace-sets data) {})) + (or (wtts/get-workspace-ordered-sets data) {})) st/state =)) (def workspace-active-theme-sets-tokens (l/derived wtts/get-active-theme-sets-tokens-names-map st/state =)) +(def workspace-ordered-token-sets-tokens + (l/derived wtts/get-workspace-ordered-sets-tokens st/state =)) + (def workspace-selected-token-set-tokens (l/derived (fn [data] diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 60803b2ee5..b9c661c3a2 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -6,6 +6,7 @@ (ns app.main.ui (:require + [app.main.ui.workspace.tokens.modals.themes :as wtmt] [app.config :as cf] [app.main.refs :as refs] [app.main.store :as st] diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs index c02783ac0f..265016a934 100644 --- a/frontend/src/app/main/ui/workspace/tokens/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs @@ -144,7 +144,7 @@ Token names should only contain letters and digits separated by . characters.")} (mf/defc form {::mf/wrap-props false} [{:keys [token token-type] :as _args}] - (let [selected-set-tokens (mf/deref refs/workspace-selected-token-set-tokens) + (let [tokens (mf/deref refs/workspace-ordered-token-sets-tokens) active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens) resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:names-map? true :cache-atom form-token-cache-atom}) @@ -152,9 +152,9 @@ Token names should only contain letters and digits separated by . characters.")} (mf/deps (:name token)) #(wtt/token-name->path (:name token))) selected-set-tokens-tree (mf/use-memo - (mf/deps token-path selected-set-tokens) + (mf/deps token-path tokens) (fn [] - (-> (wtt/token-names-tree selected-set-tokens) + (-> (wtt/token-names-tree tokens) ;; Allow setting editing token to it's own path (d/dissoc-in token-path)))) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/modals.cljs index 23edf75ede..30a45c49f9 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals.cljs @@ -8,6 +8,7 @@ (:require-macros [app.main.style :as stl]) (:require [app.main.data.modal :as modal] + [app.main.ui.workspace.tokens.modals.themes :as wtmt] [app.main.refs :as refs] [app.main.ui.workspace.tokens.form :refer [form]] [okulary.core :as l] @@ -37,7 +38,7 @@ (-> (calculate-position vport position x y) (clj->js)))) -(mf/defc modal +(mf/defc token-update-create-modal {::mf/wrap-props false} [{:keys [x y position token token-type] :as _args}] (let [wrapper-style (use-viewport-position-style x y position)] @@ -47,82 +48,88 @@ [:& form {:token token :token-type token-type}]])) +(mf/defc token-themes-modal + {::mf/register modal/components + ::mf/register-as :tokens/themes} + [args] + [:& wtmt/modal args]) + ;; Modals ---------------------------------------------------------------------- (mf/defc boolean-modal {::mf/register modal/components ::mf/register-as :tokens/boolean} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc border-radius-modal {::mf/register modal/components ::mf/register-as :tokens/border-radius} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc stroke-width-modal {::mf/register modal/components ::mf/register-as :tokens/stroke-width} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc box-shadow-modal {::mf/register modal/components ::mf/register-as :tokens/box-shadow} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc sizing-modal {::mf/register modal/components ::mf/register-as :tokens/sizing} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc dimensions-modal {::mf/register modal/components ::mf/register-as :tokens/dimensions} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc numeric-modal {::mf/register modal/components ::mf/register-as :tokens/numeric} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc opacity-modal {::mf/register modal/components ::mf/register-as :tokens/opacity} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc other-modal {::mf/register modal/components ::mf/register-as :tokens/other} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc rotation-modal {::mf/register modal/components ::mf/register-as :tokens/rotation} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc spacing-modal {::mf/register modal/components ::mf/register-as :tokens/spacing} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc string-modal {::mf/register modal/components ::mf/register-as :tokens/string} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) (mf/defc typography-modal {::mf/register modal/components ::mf/register-as :tokens/typography} [properties] - [:& modal properties]) + [:& token-update-create-modal properties]) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs new file mode 100644 index 0000000000..91c9498261 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs @@ -0,0 +1,237 @@ +;; 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 + +(ns app.main.ui.workspace.tokens.modals.themes + (:require-macros [app.main.style :as stl]) + (:require + [app.main.data.modal :as modal] + [app.main.data.tokens :as wdt] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.icons :as i] + [app.main.ui.workspace.tokens.common :refer [labeled-input]] + [app.main.ui.workspace.tokens.sets :as wts] + [app.main.ui.workspace.tokens.token-set :as wtts] + [app.util.dom :as dom] + [rumext.v2 :as mf] + [cuerdas.core :as str] + [app.main.ui.workspace.tokens.sets-context :as sets-context])) + +(def ^:private chevron-icon + (i/icon-xref :arrow (stl/css :chevron-icon))) + +(def ^:private close-icon + (i/icon-xref :close (stl/css :close-icon))) + +(mf/defc empty-themes + [{:keys [set-state]}] + [:div {:class (stl/css :empty-themes-wrapper)} + [:div {:class (stl/css :empty-themes-message)} + [:h1 "You currently have no themes."] + [:p "Create your first theme now."]] + [:div {:class (stl/css :button-footer)} + [:button {:class (stl/css :button-primary) + :on-click #(set-state (fn [_] {:type :create-theme}))} + "New theme"]]]) + +(mf/defc switch + [{:keys [selected? name on-change]}] + (let [selected (if selected? :on :off)] + [:& radio-buttons {:selected selected + :on-change on-change + :name name} + [:& radio-button {:id :on + :value :on + :icon i/tick + :label ""}] + [:& radio-button {:id :off + :value :off + :icon i/close + :label ""}]])) + +(mf/defc themes-overview + [{:keys [set-state]}] + (let [active-theme-ids (mf/deref refs/workspace-active-theme-ids) + themes (mf/deref refs/workspace-ordered-token-themes) + on-edit-theme (fn [theme e] + (dom/prevent-default e) + (dom/stop-propagation e) + (set-state (fn [_] {:type :edit-theme + :theme-id (:id theme)})))] + [:div + [:ul {:class (stl/css :theme-group-wrapper)} + (for [[group themes] themes] + [:li {:key (str "token-theme-group" group)} + (when (seq group) + [:span {:class (stl/css :theme-group-label)} group]) + [:ul {:class (stl/css :theme-group-rows-wrapper)} + (for [{:keys [id name] :as theme} themes + :let [selected? (some? (get active-theme-ids id))]] + [:li {:key (str "token-theme-" id) + :class (stl/css :theme-row)} + [:div {:class (stl/css :theme-row-left)} + [:div {:on-click (fn [e] + (dom/prevent-default e) + (dom/stop-propagation e) + (st/emit! (wdt/toggle-token-theme id)))} + [:& switch {:name (str "Theme" name) + :on-change (constantly nil) + :selected? selected?}]] + [:span {:class (stl/css :theme-row-label)} name]] + [:div {:class (stl/css :theme-row-right)} + (if-let [sets-count (some-> theme :sets seq count)] + [:button {:class (stl/css :sets-count-button) + :on-click #(on-edit-theme theme %)} + (str sets-count " sets") + chevron-icon] + [:button {:class (stl/css :sets-count-empty-button) + :on-click #(on-edit-theme theme %)} + "No sets defined" + chevron-icon]) + [:div {:class (stl/css :delete-theme-button)} + [:button {:on-click (fn [e] + (dom/prevent-default e) + (dom/stop-propagation e) + (st/emit! (wdt/delete-token-theme id)))} + i/delete]]]])]])] + [:div {:class (stl/css :button-footer)} + [:button {:class (stl/css :create-theme-button) + :on-click (fn [e] + (dom/prevent-default e) + (dom/stop-propagation e) + (set-state (fn [_] {:type :create-theme})))} + i/add + "Create theme"]]])) + +(mf/defc edit-theme + [{:keys [token-sets theme on-back on-submit] :as props}] + (let [edit? (some? (:id theme)) + theme-state (mf/use-state {:token-sets token-sets + :theme theme}) + disabled? (-> (get-in @theme-state [:theme :name]) + (str/trim) + (str/empty?)) + token-set-active? (mf/use-callback + (mf/deps theme-state) + (fn [id] + (get-in @theme-state [:theme :sets id]))) + on-toggle-token-set (mf/use-callback + (mf/deps theme-state) + (fn [token-set-id] + (swap! theme-state (fn [st] + (update st :theme #(wtts/toggle-token-set-to-token-theme token-set-id %)))))) + on-change-field (fn [field] + (fn [e] + (swap! theme-state (fn [st] (assoc-in st field (dom/get-target-val e)))))) + on-update-group (on-change-field [:theme :group]) + on-update-name (on-change-field [:theme :name]) + on-save-form (mf/use-callback + (mf/deps theme-state on-submit) + (fn [e] + (dom/prevent-default e) + (let [theme (:theme @theme-state) + final-name (str/trim (:name theme)) + final-group (-> (:group theme) + (str/trim) + (str/lower))] + (when-not (str/empty? final-name) + (cond-> theme + (empty final-group) (dissoc :group) + :always on-submit))) + (on-back)))] + [:form {:on-submit on-save-form} + [:div {:class (stl/css :edit-theme-wrapper)} + [:div + [:button {:class (stl/css :back-button) + :type "button" + :on-click on-back} + chevron-icon "Back"]] + [:div {:class (stl/css :edit-theme-inputs-wrapper)} + [:& labeled-input {:label "Group" + :input-props {:default-value (:group theme) + :on-change on-update-group}}] + [:& labeled-input {:label "Theme" + :input-props {:default-value (:name theme) + :on-change on-update-name}}]] + [:div {:class (stl/css :sets-list-wrapper)} + [:& wts/controlled-sets-list + {:token-sets token-sets + :token-set-selected? (constantly false) + :token-set-active? token-set-active? + :on-select on-toggle-token-set + :on-toggle-token-set on-toggle-token-set + :context sets-context/static-context}]] + [:div {:class (stl/css :edit-theme-footer)} + (if edit? + [:button {:class (stl/css :button-secondary) + :type "button" + :on-click (fn [] + (st/emit! (wdt/delete-token-theme (:id theme))) + (on-back))} + "Delete"] + [:div]) + [:div {:class (stl/css :button-footer)} + [:button {:class (stl/css :button-secondary) + :type "button" + :on-click #(st/emit! (modal/hide))} + "Cancel"] + [:button {:class (stl/css :button-primary) + :type "submit" + :on-click on-save-form + :disabled disabled?} + "Save theme"]]]]])) + +(mf/defc controlled-edit-theme + [{:keys [state set-state]}] + (let [{:keys [theme-id]} @state + token-sets (mf/deref refs/workspace-ordered-token-sets) + theme (mf/deref (refs/workspace-token-theme theme-id))] + [:& edit-theme + {:token-sets token-sets + :theme theme + :on-back #(set-state (constantly {:type :themes-overview})) + :on-submit #(st/emit! (wdt/update-token-theme %))}])) + +(mf/defc create-theme + [{:keys [set-state]}] + (let [token-sets (mf/deref refs/workspace-ordered-token-sets) + theme {:name "" :sets #{}}] + [:& edit-theme + {:token-sets token-sets + :theme theme + :on-back #(set-state (constantly {:type :themes-overview})) + :on-submit #(st/emit! (wdt/create-token-theme %))}])) + +(mf/defc themes + [{:keys [] :as _args}] + (let [themes (mf/deref refs/workspace-ordered-token-themes) + state (mf/use-state (if (empty? themes) + {:type :create-theme} + {:type :themes-overview})) + set-state (mf/use-callback #(swap! state %)) + title (case (:type @state) + :edit-theme "Edit Theme" + "Themes") + component (case (:type @state) + :empty-themes empty-themes + :themes-overview (if (empty? themes) empty-themes themes-overview) + :edit-theme controlled-edit-theme + :create-theme create-theme)] + [:div + [:div {:class (stl/css :modal-title)} title] + [:div {:class (stl/css :modal-content)} + [:& component {:state state + :set-state set-state}]]])) + +(mf/defc modal + {::mf/wrap-props false} + [{:keys [] :as _args}] + (let [handle-close-dialog (mf/use-callback #(st/emit! (modal/hide)))] + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon] + [:& themes]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss new file mode 100644 index 0000000000..43844c3927 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss @@ -0,0 +1,213 @@ +// 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"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +hr { + border-color: var(--color-background-tertiary); +} + +.modal-dialog { + @extend .modal-container-base; + display: grid; + grid-template-rows: auto 1fr auto; + width: 100%; + max-width: $s-468; + user-select: none; +} + +.modal-title { + @include headlineMediumTypography; + font-weight: 500; + margin-block-end: $s-16; + color: var(--color-foreground-secondary); +} + +.modal-content { + display: flex; + flex-direction: column; +} + +.button-footer { + display: flex; + justify-content: flex-end; + gap: $s-6; +} + +.edit-theme-footer { + display: flex; + justify-content: space-between; +} + +.button-primary { + @extend .button-primary; + padding: $s-6; +} + +.button-secondary { + @extend .button-secondary; + padding: $s-6; +} + +.empty-themes-wrapper { + display: flex; + flex-direction: column; + + .empty-themes-message { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: $s-12; + padding: $s-72 0; + + h1 { + @include headlineLargeTypography; + } + + p { + @include bodyMediumTypography; + font-weight: 500; + color: var(--color-foreground-secondary); + } + } +} + +.create-theme-button { + @extend .button-secondary; + padding: $s-6; + padding-right: $s-8; + svg { + margin-right: $s-6; + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.close-btn { + @extend .modal-close-btn-base; +} + +.theme-group-label { + display: block; + @include headlineMediumTypography; + color: var(--color-foreground-secondary); + margin-bottom: $s-8; +} + +.theme-group-rows-wrapper { + display: flex; + flex-direction: column; + gap: $s-6; +} + +.theme-group-wrapper { + display: flex; + flex-direction: column; + gap: $s-8; +} + +.theme-row { + display: flex; + align-items: center; + gap: $s-12; + justify-content: space-between; +} + +.theme-row-left { + display: flex; + align-items: center; + gap: $s-16; +} + +.theme-row-right { + display: flex; + align-items: center; + gap: $s-6; +} + +.back-button { + @extend .button-tertiary; + padding: $s-6; + padding-left: 0; + display: flex; + svg { + scale: -1 1; + margin-left: 0; + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.sets-count-button { + @extend .button-secondary; + padding: $s-6; + padding-left: $s-12; + svg { + margin-left: $s-6; + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.edit-theme-wrapper { + display: flex; + flex-direction: column; + gap: $s-12; +} + +.edit-theme-inputs-wrapper { + display: grid; + grid-template-columns: 0.6fr 1fr; + gap: $s-12; +} + +.sets-list-wrapper { + border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent); + border-radius: $s-8; + overflow: hidden; +} + +.sets-count-empty-button { + @extend .button-secondary; + padding: $s-6; + padding-left: $s-12; + svg { + margin-left: $s-6; + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.theme-row-label { + @include bodyMediumTypography; + font-weight: 500; + color: var(--color-foreground-primary); +} + +.delete-theme-button { + @extend .button-tertiary; + height: $s-28; + width: $s-28; + button { + @include buttonStyle; + @include flexCenter; + width: $s-24; + height: 100%; + svg { + @extend .button-icon-small; + height: $s-12; + width: $s-12; + color: transparent; + fill: none; + stroke: var(--icon-foreground); + } + } +} diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index 852844bf51..184c38cce7 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -12,32 +12,81 @@ [app.main.store :as st] [app.main.ui.icons :as i] [app.util.dom :as dom] - [rumext.v2 :as mf])) + [app.util.keyboard :as kbd] + [cuerdas.core :as str] + [rumext.v2 :as mf] + [app.main.ui.workspace.tokens.sets-context :as sets-context])) (def ^:private chevron-icon (i/icon-xref :arrow (stl/css :chevron-icon))) -(defn on-toggle-token-set-click [id event] - (dom/stop-propagation event) - (st/emit! (wdt/toggle-token-set id))) +(defn on-toggle-token-set-click [token-set-id] + (st/emit! (wdt/toggle-token-set {:token-set-id token-set-id}))) -(defn on-select-token-set-click [id event] +(defn on-select-token-set-click [id] (st/emit! (wdt/set-selected-token-set-id id))) (defn on-delete-token-set-click [id event] (dom/stop-propagation event) (st/emit! (wdt/delete-token-set id))) +(defn on-update-token-set [token-set] + (st/emit! (wdt/update-token-set token-set))) + +(defn on-create-token-set [token-set] + (st/emit! (wdt/create-token-set token-set))) + +(mf/defc editing-node + [{:keys [default-value on-cancel on-submit]}] + (let [ref (mf/use-ref) + on-submit-valid (mf/use-fn + (fn [event] + (let [value (str/trim (dom/get-target-val event))] + (if (or (str/empty? value) + (= value default-value)) + (on-cancel) + (on-submit value))))) + on-key-down (mf/use-fn + (fn [event] + (cond + (kbd/enter? event) (on-submit-valid event) + (kbd/esc? event) (on-cancel))))] + [:input + {:class (stl/css :editing-node) + :type "text" + :ref ref + :on-blur on-submit-valid + :on-key-down on-key-down + :auto-focus true + :default-value default-value}])) + (mf/defc sets-tree - [{:keys [token-set token-set-active? token-set-selected?] :as _props}] + [{:keys [token-set + token-set-active? + token-set-selected? + editing? + on-select + on-toggle + on-edit + on-submit + on-cancel] + :as _props}] (let [{:keys [id name _children]} token-set selected? (and set? (token-set-selected? id)) visible? (token-set-active? id) collapsed? (mf/use-state false) set? true #_(= type :set) - group? false #_(= type :group)] + group? false #_(= type :group) + editing-node? (editing? id) + on-select (mf/use-callback + (mf/deps editing-node?) + (fn [event] + (dom/stop-propagation event) + (when-not editing-node? + (on-select id))))] [:div {:class (stl/css :set-item-container) - :on-click #(on-select-token-set-click id %)} + :on-click on-select + :on-double-click #(on-edit id)} [:div {:class (stl/css-case :set-item-group group? :set-item-set set? :selected-set selected?)} @@ -48,25 +97,72 @@ chevron-icon]) [:span {:class (stl/css :icon)} (if set? i/document i/group)] - [:div {:class (stl/css :set-name)} name] - [:div {:class (stl/css :delete-set)} - [:button {:on-click #(on-delete-token-set-click id %)} - i/delete]] - (when set? - [:span {:class (stl/css :action-btn) - :on-click #(on-toggle-token-set-click id %)} - (if visible? i/shown i/hide)])] - #_(when (and children (not @collapsed?)) - [:div {:class (stl/css :set-children)} - (for [child-id children] - [:& sets-tree (assoc props :key child-id - {:key child-id} - :set-id child-id - :selected-set-id selected-token-set-id)])])])) + (if editing-node? + [:& editing-node {:default-value name + :on-submit #(on-submit (assoc token-set :name %)) + :on-cancel on-cancel}] + [:* + [:div {:class (stl/css :set-name)} name] + [:div {:class (stl/css :delete-set)} + [:button {:on-click #(on-delete-token-set-click id %) + :type "button"} + i/delete]] + (if set? + [:span {:class (stl/css :action-btn) + :on-click (fn [event] + (dom/stop-propagation event) + (on-toggle id))} + (if visible? i/shown i/hide)] + nil + #_(when (and children (not @collapsed?)) + [:div {:class (stl/css :set-children)} + (for [child-id children] + [:& sets-tree (assoc props :key child-id + {:key child-id} + :set-id child-id + :selected-set-id selected-token-set-id)])]))])]])) + +(mf/defc controlled-sets-list + [{:keys [token-sets + on-update-token-set + token-set-selected? + token-set-active? + on-create-token-set + on-toggle-token-set + on-select + context] + :as _props}] + (let [{:keys [editing? new? on-edit on-create on-reset] :as ctx} (or context (sets-context/use-context))] + [:ul {:class (stl/css :sets-list)} + (for [[id token-set] token-sets] + (when token-set + [:& sets-tree {:key id + :token-set token-set + :token-set-selected? (if new? (constantly false) token-set-selected?) + :token-set-active? token-set-active? + :editing? editing? + :on-select on-select + :on-edit on-edit + :on-toggle on-toggle-token-set + :on-submit #(do + (on-update-token-set %) + (on-reset)) + :on-cancel on-reset}])) + (when new? + [:& sets-tree {:token-set {:name ""} + :token-set-selected? (constantly true) + :token-set-active? (constantly true) + :editing? (constantly true) + :on-select (constantly nil) + :on-edit on-create + :on-submit #(do + (on-create-token-set %) + (on-reset)) + :on-cancel on-reset}])])) (mf/defc sets-list [{:keys []}] - (let [token-sets (mf/deref refs/workspace-token-sets) + (let [token-sets (mf/deref refs/workspace-ordered-token-sets) selected-token-set-id (mf/deref refs/workspace-selected-token-set-id) token-set-selected? (mf/use-callback (mf/deps selected-token-set-id) @@ -77,11 +173,11 @@ (mf/deps active-token-set-ids) (fn [id] (get active-token-set-ids id)))] - [:ul {:class (stl/css :sets-list)} - (for [[id token-set] token-sets] - [:& sets-tree - {:key id - :token-set token-set - :selected-token-set-id selected-token-set-id - :token-set-selected? token-set-selected? - :token-set-active? token-set-active?}])])) + [:& controlled-sets-list + {:token-sets token-sets + :token-set-selected? token-set-selected? + :token-set-active? token-set-active? + :on-select on-select-token-set-click + :on-toggle-token-set on-toggle-token-set-click + :on-update-token-set on-update-token-set + :on-create-token-set on-create-token-set}])) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.scss b/frontend/src/app/main/ui/workspace/tokens/sets.scss index a2049236e0..c65fdd2c35 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets.scss @@ -126,3 +126,21 @@ transform: rotate(var(--chevron-icon-rotation)); stroke: var(--icon-foreground); } + +.editing-node { + @include textEllipsis; + color: var(--layer-row-foreground-color-focus); +} + +input.editing-node { + @include textEllipsis; + @include bodySmallTypography; + @include removeInputStyle; + flex-grow: 1; + height: $s-28; + padding-left: $s-6; + margin: 0; + border-radius: $br-8; + border: $s-1 solid var(--input-border-color-focus); + color: var(--layer-row-foreground-color); +} diff --git a/frontend/src/app/main/ui/workspace/tokens/sets_context.cljs b/frontend/src/app/main/ui/workspace/tokens/sets_context.cljs new file mode 100644 index 0000000000..b5920f335b --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/sets_context.cljs @@ -0,0 +1,41 @@ +(ns app.main.ui.workspace.tokens.sets-context + (:require + [rumext.v2 :as mf])) + +(def initial {:editing-id nil + :new? false}) + +(def context (mf/create-context initial)) + +(def static-context + {:editing? (constantly false) + :new? false + :on-edit (constantly nil) + :on-create (constantly nil) + :on-reset (constantly nil)}) + +(mf/defc provider + {::mf/wrap-props false} + [props] + (let [children (unchecked-get props "children") + state (mf/use-state initial)] + [:& (mf/provider context) {:value state} + children])) + +(defn use-context [] + (let [ctx (mf/use-ctx context) + {:keys [editing-id new?]} @ctx + editing? (mf/use-callback + (mf/deps editing-id) + #(= editing-id %)) + on-edit (mf/use-fn + #(swap! ctx assoc :editing-id %)) + on-create (mf/use-fn + #(swap! ctx assoc :editing-id (random-uuid) :new? true)) + on-reset (mf/use-fn + #(reset! ctx initial))] + {:editing? editing? + :new? new? + :on-edit on-edit + :on-create on-create + :on-reset on-reset})) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index b9e6c6a87a..45a12a5cd1 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -14,6 +14,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.title-bar :refer [title-bar]] + [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.main.ui.workspace.tokens.changes :as wtch] @@ -21,7 +22,9 @@ [app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.core :as wtc] [app.main.ui.workspace.tokens.sets :refer [sets-list]] + [app.main.ui.workspace.tokens.sets-context :as sets-context] [app.main.ui.workspace.tokens.style-dictionary :as sd] + [app.main.ui.workspace.tokens.theme-select :refer [theme-select]] [app.main.ui.workspace.tokens.token :as wtt] [app.main.ui.workspace.tokens.token-types :as wtty] [app.util.dom :as dom] @@ -107,6 +110,7 @@ (let [{:keys [key fields]} modal] (dom/stop-propagation event) (st/emit! (dt/set-token-type-section-open type true)) + (js/console.log "key" key) (modal/show! key {:x (.-clientX ^js event) :y (.-clientY ^js event) :position :right @@ -179,74 +183,47 @@ :name @name}))} "Create"]])) +(mf/defc edit-button + [{:keys [create?]}] + [:button {:class (stl/css :themes-button) + :on-click (fn [e] + (dom/stop-propagation e) + (modal/show! :tokens/themes {}))} + (if create? "Create" "Edit")]) + (mf/defc themes-sidebar [_props] - (let [open? (mf/use-state true) - active-theme-ids (mf/deref refs/workspace-active-theme-ids) - themes (mf/deref refs/workspace-ordered-token-themes)] - [:div {:class (stl/css :sets-sidebar)} - [:div {:class (stl/css :sidebar-header)} - [:& title-bar {:collapsable true - :collapsed (not @open?) - :all-clickable true - :title "THEMES" - :on-collapsed #(swap! open? not)}]] - (when @open? - [:div - [:style - (str "@scope {" - (str/join "\n" - ["ul { list-style-type: circle; margin-left: 20px; }" - ".spaced { display: flex; gap: 10px; justify-content: space-between; }" - ".spaced-y { display: flex; flex-direction: column; gap: 10px }" - ".selected { font-weight: 600; }" - "b { font-weight: 600; }"]) - "}")] - [:div.spaced-y - {:style {:padding "10px"}} - [:& tokene-theme-create] - [:div.spaced-y - [:b "Themes"] - [:ul - (for [[group themes] themes] - [:li - {:key (str "token-theme-group" group)} - group - [:ul - (for [{:keys [id name] :as _theme} themes] - [:li {:key (str "tokene-theme-" id)} - [:div.spaced - name - [:div.spaced - [:button - {:on-click (fn [e] - (dom/prevent-default e) - (dom/stop-propagation e) - (st/emit! (wdt/toggle-token-theme id)))} - (if (get active-theme-ids id) "✅" "❎")] - [:button {:on-click (fn [e] - (dom/prevent-default e) - (dom/stop-propagation e) - (st/emit! (wdt/delete-token-theme id)))} - "🗑️"]]]])]])]]]])])) + (let [ordered-themes (mf/deref refs/workspace-ordered-token-themes)] + [:div {:class (stl/css :theme-sidebar)} + [:span {:class (stl/css :themes-header)} "Themes"] + [:div {:class (stl/css :theme-select-wrapper)} + [:& theme-select] + [:& edit-button {:create? (empty? ordered-themes)}]]])) + +(mf/defc add-set-button + [{:keys [on-open]}] + (let [{:keys [on-create]} (sets-context/use-context)] + [:button {:class (stl/css :add-set) + :on-click #(do + (on-open) + (on-create))} + i/add])) (mf/defc sets-sidebar [] - (let [open? (mf/use-state true)] - [:div {:class (stl/css :sets-sidebar)} - [:div {:class (stl/css :sidebar-header)} - [:& title-bar {:collapsable true - :collapsed (not @open?) - :all-clickable true - :title "SETS" - :on-collapsed #(swap! open? not)}] - [:button {:class (stl/css :add-set) - :on-click #(do - (reset! open? true) - (on-set-add-click %))} - i/add]] - (when @open? - [:& sets-list])])) + (let [open? (mf/use-state true) + on-open (mf/use-fn #(reset! open? true))] + [:& sets-context/provider {} + [:div {:class (stl/css :sets-sidebar)} + [:div {:class (stl/css :sidebar-header)} + [:& title-bar {:collapsable true + :collapsed (not @open?) + :all-clickable true + :title "SETS" + :on-collapsed #(swap! open? not)} + [:& add-set-button {:on-open on-open}]]] + (when @open? + [:& sets-list])]])) (mf/defc tokens-explorer [_props] @@ -306,13 +283,24 @@ {::mf/wrap [mf/memo] ::mf/wrap-props false} [_props] - (let [show-sets-section? (deref (temp-use-themes-flag))] + (let [show-sets-section? (deref (temp-use-themes-flag)) + {on-pointer-down-pages :on-pointer-down + on-lost-pointer-capture-pages :on-lost-pointer-capture + on-pointer-move-pages :on-pointer-move + size-pages-opened :size} + (use-resize-hook :sitemap 200 38 400 :y false nil)] [:div {:class (stl/css :sidebar-tab-wrapper)} (when show-sets-section? - [:div {:class (stl/css :sets-section-wrapper)} + [:div {:class (stl/css :sets-section-wrapper) + :style {:height (str size-pages-opened "px")}} [:& themes-sidebar] [:& sets-sidebar]]) [:div {:class (stl/css :tokens-section-wrapper)} + (when show-sets-section? + [:div {:class (stl/css :resize-area-horiz) + :on-pointer-down on-pointer-down-pages + :on-lost-pointer-capture on-lost-pointer-capture-pages + :on-pointer-move on-pointer-move-pages}]) [:& tokens-explorer]] [:button {:class (stl/css :download-json-button) :on-click wtc/download-tokens-as-json} diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index e90b12d3cf..092a8bc862 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -18,12 +18,18 @@ display: flex; flex-direction: column; margin-bottom: $s-8; + overflow-y: auto; } .sets-sidebar { position: relative; } +.theme-sidebar { + padding: $s-12; + padding-bottom: 0; +} + .sidebar-header { display: flex; align-items: center; @@ -114,3 +120,30 @@ height: 20px; } } + +.theme-select-wrapper { + display: grid; + grid-template-columns: 1fr 0.28fr; + gap: $s-6; +} + +.themes-button { + @extend .button-secondary; + width: auto; +} + +.themes-header { + display: block; + @include headlineSmallTypography; + margin-bottom: $s-8; + padding-left: $s-8; + color: var(--title-foreground-color); +} + +.resize-area-horiz { + position: absolute; + left: 0; + width: 100%; + border-bottom: $s-2 solid var(--resize-area-border-color); + cursor: ns-resize; +} diff --git a/frontend/src/app/main/ui/workspace/tokens/theme_select.cljs b/frontend/src/app/main/ui/workspace/tokens/theme_select.cljs new file mode 100644 index 0000000000..0f0cc2a4e4 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/theme_select.cljs @@ -0,0 +1,95 @@ +;; 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 + +(ns app.main.ui.workspace.tokens.theme-select + (:require-macros [app.main.style :as stl]) + (:require + [app.common.uuid :as uuid] + [app.main.data.modal :as modal] + [app.main.data.tokens :as wdt] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(mf/defc themes-list + [{:keys [themes active-theme-ids on-close grouped?]}] + (when (seq themes) + [:ul + (for [{:keys [id name]} themes + :let [selected? (get active-theme-ids id)]] + [:li {:key id + :class (stl/css-case + :checked-element true + :sub-item grouped? + :is-selected selected?) + :on-click (fn [e] + (dom/stop-propagation e) + (st/emit! (wdt/toggle-token-theme id)) + (on-close))} + [:span {:class (stl/css :label)} name] + [:span {:class (stl/css :check-icon)} i/tick]])])) + +(mf/defc theme-options + [{:keys [on-close]}] + (let [active-theme-ids (mf/deref refs/workspace-active-theme-ids) + ordered-themes (mf/deref refs/workspace-ordered-token-themes) + grouped-themes (dissoc ordered-themes nil) + ungrouped-themes (get ordered-themes nil)] + [:ul + [:& themes-list {:themes ungrouped-themes + :active-theme-ids active-theme-ids + :on-close on-close}] + (for [[group themes] grouped-themes] + [:li {:key group} + (when group + [:span {:class (stl/css :group)} group]) + [:& themes-list {:themes themes + :active-theme-ids active-theme-ids + :on-close on-close + :grouped? true}]]) + [:li {:class (stl/css-case :checked-element true + :checked-element-button true) + :on-click #(modal/show! :tokens/themes {})} + [:span "Edit themes"] + [:span {:class (stl/css :icon)} i/arrow]]])) + +(mf/defc theme-select + [{:keys []}] + (let [;; Store + temp-theme-id (mf/deref refs/workspace-temp-theme-id) + active-theme-ids (-> (mf/deref refs/workspace-active-theme-ids) + (disj temp-theme-id)) + active-themes-count (count active-theme-ids) + themes (mf/deref refs/workspace-token-themes) + + ;; Data + current-label (cond + (> active-themes-count 1) (str active-themes-count " themes active") + (pos? active-themes-count) (get-in themes [(first active-theme-ids) :name]) + :else "No theme active") + + ;; State + state* (mf/use-state + {:id (uuid/next) + :is-open? false}) + state (deref state*) + is-open? (:is-open? state) + + ;; Dropdown + dropdown-element* (mf/use-ref nil) + on-close-dropdown (mf/use-fn #(swap! state* assoc :is-open? false)) + on-open-dropdown (mf/use-fn #(swap! state* assoc :is-open? true))] + [:div {:on-click on-open-dropdown + :class (stl/css :custom-select)} + [:span {:class (stl/css :current-label)} current-label] + [:span {:class (stl/css :dropdown-button)} i/arrow] + [:& dropdown {:show is-open? :on-close on-close-dropdown} + [:div {:ref dropdown-element* + :class (stl/css :custom-select-dropdown)} + [:& theme-options {:on-close on-close-dropdown}]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/theme_select.scss b/frontend/src/app/main/ui/workspace/tokens/theme_select.scss new file mode 100644 index 0000000000..da8a7b5a39 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/theme_select.scss @@ -0,0 +1,167 @@ +// 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"; + +.custom-select { + --border-color: var(--menu-background-color); + --bg-color: var(--menu-background-color); + --icon-color: var(--icon-foreground); + --text-color: var(--menu-foreground-color); + @extend .new-scrollbar; + @include bodySmallTypography; + position: relative; + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + height: $s-32; + width: 100%; + margin: 0; + padding: $s-8; + border-radius: $br-8; + background-color: var(--bg-color); + border: $s-1 solid var(--border-color); + color: var(--text-color); + cursor: pointer; + + ul { + margin-bottom: 0; + } + + .group { + display: block; + @include headlineSmallTypography; + padding: $s-8; + color: var(--color-foreground-secondary); + font-weight: 600; + } + + &.icon { + grid-template-columns: auto 1fr auto; + } + + &:hover { + --bg-color: var(--menu-background-color-hover); + --border-color: var(--menu-background-color); + --icon-color: var(--menu-foreground-color-hover); + } + + &:focus { + --bg-color: var(--menu-background-color-focus); + --border-color: var(--menu-background-focus); + } +} + +.disabled { + --bg-color: var(--menu-background-color-disabled); + --border-color: var(--menu-border-color-disabled); + --icon-color: var(--menu-foreground-color-disabled); + --text-color: var(--menu-foreground-color-disabled); + pointer-events: none; + cursor: default; +} + +.dropdown-button { + @include flexCenter; + svg { + @extend .button-icon-small; + transform: rotate(90deg); + stroke: var(--icon-color); + } +} + +.current-icon { + @include flexCenter; + width: $s-24; + padding-right: $s-4; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.custom-select-dropdown { + @extend .dropdown-wrapper; + .separator { + margin: 0; + height: $s-12; + border-block-start: $s-1 solid var(--dropdown-separator-color); + } +} + +.custom-select-dropdown[data-direction="up"] { + bottom: $s-32; + top: auto; +} + +.sub-item { + padding-left: $s-16; +} + +.checked-element-button { + @extend .dropdown-element-base; + position: relative; + display: flex; + justify-content: space-between; + padding-right: 0; +} + +li + .checked-element-button { + margin-top: $s-8; + &:before { + content: ""; + position: absolute; + top: -$s-4; + left: 0; + right: 0; + height: 1px; + background-color: color-mix(in hsl, var(--color-foreground-secondary) 20%, transparent); + } +} + +.checked-element { + @extend .dropdown-element-base; + + .icon { + @include flexCenter; + height: $s-24; + width: $s-24; + padding-right: $s-4; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + + .label { + flex-grow: 1; + width: 100%; + } + + .check-icon { + @include flexCenter; + svg { + @extend .button-icon-small; + visibility: hidden; + stroke: var(--icon-foreground); + } + } + + &.is-selected { + color: var(--menu-foreground-color); + .check-icon svg { + stroke: var(--menu-foreground-color); + visibility: visible; + } + } + &.disabled { + display: none; + } +} + +.current-label { + @include textEllipsis; +} diff --git a/frontend/src/app/main/ui/workspace/tokens/token_set.cljs b/frontend/src/app/main/ui/workspace/tokens/token_set.cljs index 6a6a737c83..62eb5def4b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token_set.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token_set.cljs @@ -12,6 +12,9 @@ (defn get-workspace-themes [state] (get-in state [:workspace-data :token-themes] [])) +(defn get-workspace-theme [id state] + (get-in state [:workspace-data :token-themes-index id])) + (defn get-workspace-themes-index [state] (get-in state [:workspace-data :token-themes-index] {})) @@ -69,7 +72,7 @@ (defn toggle-active-theme-id "Toggle a `theme-id` by checking `:token-active-themes`. - De-activate all theme-ids that have the same group as `theme-id` when activating `theme-id`. + Deactivate all theme-ids that have the same group as `theme-id` when activating `theme-id`. Ensures that the temporary theme id is selected when the resulting set is empty." [theme-id state] (let [temp-theme-id-set (some->> (get-temp-theme-id state) (conj #{})) @@ -112,6 +115,24 @@ (defn get-workspace-sets [state] (get-in state [:workspace-data :token-sets-index])) +(defn get-workspace-ordered-sets [state] + ;; TODO Include groups + (let [top-level-set-ids (get-in state [:workspace-data :token-set-groups]) + token-sets (get-workspace-sets state)] + (->> (map (fn [id] [id (get token-sets id)]) top-level-set-ids) + (into (ordered-map))))) + +(defn get-workspace-ordered-sets-tokens [state] + (let [sets (get-workspace-ordered-sets state)] + (reduce + (fn [acc [_ {:keys [tokens] :as sets}]] + (reduce (fn [acc' token-id] + (if-let [token (wtt/get-workspace-token token-id state)] + (assoc acc' (wtt/token-identifier token) token) + acc')) + acc tokens)) + {} sets))) + (defn get-token-set [set-id state] (some-> (get-workspace-sets state) (get set-id)))