From ce59070fd1bfd0ee625be291533bc005e0fe1be0 Mon Sep 17 00:00:00 2001 From: Xavier Julian Date: Thu, 19 Jun 2025 14:03:26 +0200 Subject: [PATCH] :recycle: Restructure UI files for token sets --- frontend/playwright/ui/pages/WorkspacePage.js | 2 +- .../app/main/ui/workspace/tokens/sets.cljs | 521 +----------------- .../context_menu.cljs} | 2 +- .../context_menu.scss} | 2 +- .../ui/workspace/tokens/sets/helpers.cljs | 36 ++ .../main/ui/workspace/tokens/sets/lists.cljs | 492 +++++++++++++++++ .../main/ui/workspace/tokens/sets/lists.scss | 164 ++++++ .../app/main/ui/workspace/tokens/sidebar.cljs | 26 +- .../app/main/ui/workspace/tokens/sidebar.scss | 9 +- .../workspace/tokens/themes/create_modal.cljs | 2 +- 10 files changed, 719 insertions(+), 537 deletions(-) rename frontend/src/app/main/ui/workspace/tokens/{sets_context_menu.cljs => sets/context_menu.cljs} (98%) rename frontend/src/app/main/ui/workspace/tokens/{sets_context_menu.scss => sets/context_menu.scss} (95%) create mode 100644 frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/sets/lists.scss diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 2fa0433d07..a57fe35b6d 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -99,7 +99,7 @@ export class WorkspacePage extends BaseWebSocketPage { this.tokenThemeUpdateCreateModal = page.getByTestId( "token-theme-update-create-modal", ); - this.tokenThemesSetsSidebar = page.getByTestId("token-themes-sets-sidebar"); + this.tokenThemesSetsSidebar = page.getByTestId("token-management-sidebar"); this.tokensSidebar = page.getByTestId("tokens-sidebar"); this.tokenSetItems = page.getByTestId("tokens-set-item"); this.tokenSetGroupItems = page.getByTestId("tokens-set-group-item"); diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index 1c89dbd88b..be7ce5fbae 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -5,33 +5,18 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.tokens.sets - (:require-macros [app.main.style :as stl]) (:require - [app.common.data.macros :as dm] [app.common.types.tokens-lib :as ctob] - [app.main.data.event :as ev] [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] - [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] - [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic] - [app.main.ui.ds.foundations.typography.text :refer [text*]] - [app.main.ui.hooks :as h] - [app.util.dom :as dom] - [app.util.i18n :refer [tr]] - [app.util.keyboard :as kbd] - [cuerdas.core :as str] - [potok.v2.core :as ptk] + [app.main.ui.workspace.tokens.sets.helpers :as sets-helpers] + [app.main.ui.workspace.tokens.sets.lists :refer [controlled-sets-list*]] [rumext.v2 :as mf])) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; HELPERS -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- on-start-creation - [] - (st/emit! (dwtl/start-token-set-creation []))) +(defn- on-select-token-set-click [name] + (st/emit! (dwtl/set-selected-token-set-name name))) (defn- on-toggle-token-set-click [name] (st/emit! (dwtl/toggle-token-set name))) @@ -39,498 +24,6 @@ (defn- on-toggle-token-set-group-click [path] (st/emit! (dwtl/toggle-token-set-group path))) -(defn- on-select-token-set-click [name] - (st/emit! (dwtl/set-selected-token-set-name name))) - -(defn on-update-token-set - [token-set name] - (st/emit! (dwtl/clear-token-set-edition) - (dwtl/update-token-set token-set name))) - -(defn- on-update-token-set-group - [path name] - (st/emit! (dwtl/clear-token-set-edition) - (dwtl/rename-token-set-group path name))) - -(defn- on-create-token-set - [parent-set name] - (let [;; FIXME: this code should be reusable under helper under - ;; common types namespace - name - (if-let [parent-path (ctob/get-set-path parent-set)] - (->> (concat parent-path (ctob/split-set-name name)) - (ctob/join-set-path)) - (ctob/normalize-set-name name))] - - (st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name}) - (dwtl/create-token-set name)))) - -(defn group-edition-id - "Prefix editing groups `edition-id` so it can be differentiated from sets with the same id." - [edition-id] - (str "group-" edition-id)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; COMPONENTS -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(mf/defc editing-label* - {::mf/private true} - [{:keys [default-value on-cancel on-submit]}] - (let [on-submit - (mf/use-fn - (mf/deps on-cancel on-submit default-value) - (fn [event] - (let [value (dom/get-target-val event)] - (if (or (str/empty? value) - (= value default-value)) - (on-cancel) - (on-submit value))))) - - on-key-down - (mf/use-fn - (mf/deps on-submit on-cancel) - (fn [event] - (cond - (kbd/enter? event) (on-submit event) - (kbd/esc? event) (on-cancel))))] - [:input - {:class (stl/css :editing-node) - :type "text" - :on-blur on-submit - :on-key-down on-key-down - :maxlength "256" - :auto-focus true - :placeholder (tr "workspace.tokens.set-edit-placeholder") - :default-value default-value}])) - -(mf/defc checkbox* - [{:keys [checked aria-label on-click disabled]}] - (let [all? (true? checked) - mixed? (= checked "mixed") - checked? (or all? mixed?)] - - [:div {:role "checkbox" - :aria-checked (dm/str checked) - :disabled disabled - :title (when disabled (tr "workspace.tokens.no-permisions-set")) - :tab-index 0 - :class (stl/css-case :checkbox-style true - :checkbox-checked-style checked? - :checkbox-disabled-checked (and checked? disabled) - :checkbox-disabled disabled) - :on-click (when-not disabled on-click)} - - (when ^boolean checked? - [:> icon* - {:aria-label aria-label - :class (stl/css :check-icon) - :size "s" - :icon-id (if mixed? ic/remove ic/tick)}])])) - -(mf/defc inline-add-button* - [] - (let [can-edit? (mf/use-ctx ctx/can-edit?)] - (if can-edit? - [:div {:class (stl/css :empty-sets-wrapper)} - [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} - (tr "workspace.tokens.no-sets-yet")] - [:button {:on-click on-start-creation - :class (stl/css :create-set-button)} - (tr "workspace.tokens.create-one")]] - [:div {:class (stl/css :empty-sets-wrapper)} - [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} - (tr "workspace.tokens.no-sets-yet")]]))) - -(mf/defc add-button* - [] - [:> icon-button* {:variant "ghost" - :icon "add" - :on-click on-start-creation - :aria-label (tr "workspace.tokens.add set")}]) - -(mf/defc sets-tree-set-group* - {::mf/private true} - [{:keys [id label tree-depth tree-path is-active is-selected is-draggable is-collapsed tree-index on-drop - on-toggle-collapse on-toggle is-editing on-start-edition on-reset-edition on-edit-submit]}] - - (let [can-edit? - (mf/use-ctx ctx/can-edit?) - - label-id - (str id "-label") - - on-context-menu - (mf/use-fn - (mf/deps is-editing id tree-path can-edit?) - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (when (and can-edit? (not is-editing)) - (st/emit! (dwtl/assign-token-set-context-menu - {:position (dom/get-client-position event) - :is-group true - :id id - :edition-id (group-edition-id id) - :path tree-path}))))) - - on-collapse-click - (mf/use-fn - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (on-toggle-collapse tree-path))) - - on-double-click - (mf/use-fn (mf/deps id) #(on-start-edition (group-edition-id id))) - - on-checkbox-click - (mf/use-fn - (mf/deps on-toggle tree-path can-edit?) - #(on-toggle tree-path)) - - on-edit-submit' - (mf/use-fn - (mf/deps tree-path on-edit-submit can-edit?) - #(on-edit-submit tree-path %)) - - on-drop - (mf/use-fn - (mf/deps tree-index on-drop) - (fn [position data] - (on-drop tree-index position data))) - - [dprops dref] - (h/use-sortable - :data-type "penpot/token-set" - :on-drop on-drop - :data {:index tree-index - :is-group true} - :detect-center? true - :draggable? is-draggable)] - - [:div {:ref dref - :data-testid "tokens-set-group-item" - :style {"--tree-depth" tree-depth} - :class (stl/css-case :set-item-container true - :set-item-group true - :selected-set is-selected - :dnd-over (= (:over dprops) :center) - :dnd-over-top (= (:over dprops) :top) - :dnd-over-bot (= (:over dprops) :bot)) - :on-context-menu on-context-menu} - [:> icon-button* - {:class (stl/css :set-item-group-collapse-button) - :on-click on-collapse-click - :data-testid "tokens-set-group-collapse" - :aria-label (tr "labels.collapse") - :icon (if is-collapsed "arrow-right" "arrow-down") - :variant "action"}] - (if is-editing - [:> editing-label* - {:default-value label - :on-cancel on-reset-edition - :on-submit on-edit-submit'}] - [:* - [:div {:class (stl/css :set-name) - :role "button" - :title label - :tab-index 0 - :on-double-click on-double-click - :id label-id} - label] - [:> checkbox* - {:on-click on-checkbox-click - :disabled (not can-edit?) - :checked (case is-active - :all true - :partial "mixed" - :none false) - :arial-label (tr "workspace.tokens.select-set")}]])])) - -(mf/defc sets-tree-set* - [{:keys [id set label tree-depth tree-path tree-index is-selected is-active is-draggable is-editing - on-select on-drop on-toggle on-start-edition on-reset-edition on-edit-submit]}] - - (let [set-name (get set :name) - can-edit? (mf/use-ctx ctx/can-edit?) - - on-click - (mf/use-fn - (mf/deps is-editing tree-path) - (fn [event] - (dom/stop-propagation event) - (when-not is-editing - (when (fn? on-select) - (on-select set-name))))) - - on-context-menu - (mf/use-fn - (mf/deps is-editing id tree-path can-edit?) - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (when (and can-edit? (not is-editing)) - (st/emit! (dwtl/assign-token-set-context-menu - {:position (dom/get-client-position event) - :is-group false - :id id - :edition-id id - :path tree-path}))))) - - on-double-click - (mf/use-fn (mf/deps id) #(on-start-edition id)) - - on-checkbox-click - (mf/use-fn - (mf/deps set-name on-toggle) - (fn [event] - (dom/stop-propagation event) - (when (fn? on-toggle) - (on-toggle set-name)))) - - on-edit-submit' - (mf/use-fn - (mf/deps set on-edit-submit) - #(on-edit-submit set %)) - - on-drag - (mf/use-fn - (mf/deps tree-path) - (fn [_] - (when-not is-selected - (on-select tree-path)))) - - on-drop - (mf/use-fn - (mf/deps tree-index on-drop) - (fn [position data] - (on-drop tree-index position data))) - - [dprops dref] - (h/use-sortable - :data-type "penpot/token-set" - :on-drag on-drag - :on-drop on-drop - :data {:index tree-index - :is-group false} - :draggable? is-draggable) - - drop-over - (get dprops :over)] - - [:div {:ref dref - :role "button" - :data-testid "tokens-set-item" - :style {"--tree-depth" tree-depth} - :class (stl/css-case :set-item-container true - :selected-set is-selected - :dnd-over (= drop-over :center) - :dnd-over-top (= drop-over :top) - :dnd-over-bot (= drop-over :bot)) - :on-click on-click - :on-double-click on-double-click - :on-context-menu on-context-menu - :aria-checked is-active} - - [:> icon* - {:icon-id "document" - :class (stl/css-case :icon true - :root-icon (not tree-depth))}] - (if is-editing - [:> editing-label* - {:default-value label - :on-cancel on-reset-edition - :on-submit on-edit-submit'}] - [:* - [:div {:class (stl/css :set-name)} - label] - [:> checkbox* - {:on-click on-checkbox-click - :disabled (not can-edit?) - :arial-label (tr "workspace.tokens.select-set") - :checked is-active}]])])) - -(mf/defc token-sets-tree* - [{:keys [is-draggable - selected - is-token-set-group-active - is-token-set-active - on-start-edition - on-reset-edition - on-edit-submit-set - on-edit-submit-group - on-select - on-toggle-set - on-toggle-set-group - token-sets - new-path - edition-id]}] - - (let [collapsed-paths* (mf/use-state #{}) - collapsed-paths (deref collapsed-paths*) - - collapsed? - (mf/use-fn - (mf/deps collapsed-paths) - (partial contains? collapsed-paths)) - - on-drop - (mf/use-fn - (mf/deps collapsed-paths) - (fn [tree-index position data] - (let [params {:from-index (:index data) - :to-index tree-index - :position position - :collapsed-paths collapsed-paths}] - (if (:is-group data) - (st/emit! (dwtl/drop-token-set-group params)) - (st/emit! (dwtl/drop-token-set params)))))) - - on-toggle-collapse - (mf/use-fn - (fn [path] - (swap! collapsed-paths* #(if (contains? % path) - (disj % path) - (conj % path)))))] - - (for [{:keys [id token-set index is-new is-group path parent-path depth] :as node} - (ctob/sets-tree-seq token-sets - {:skip-children-pred collapsed? - :new-at-path new-path})] - (cond - ^boolean is-group - [:> sets-tree-set-group* - {:key index - :label (peek path) - :id id - :is-active (is-token-set-group-active path) - :is-selected false - :is-draggable is-draggable - :is-editing (= edition-id (group-edition-id id)) - :is-collapsed (collapsed? path) - :on-select on-select - - :tree-path path - :tree-depth depth - :tree-index index - :tree-parent-path parent-path - - :on-drop on-drop - :on-start-edition on-start-edition - :on-reset-edition on-reset-edition - :on-edit-submit on-edit-submit-group - :on-toggle-collapse on-toggle-collapse - :on-toggle on-toggle-set-group}] - - ^boolean is-new - [:> sets-tree-set* - {:key index - :set token-set - :label "" - :id id - :is-editing true - :is-active true - :is-selected true - - :tree-path path - :tree-depth depth - :tree-index index - :tree-parent-path parent-path - - :on-drop on-drop - :on-reset-edition on-reset-edition - :on-edit-submit on-create-token-set}] - - :else - [:> sets-tree-set* - {:key index - :set token-set - :id id - :label (peek path) - :is-editing (= edition-id id) - :is-active (is-token-set-active id) - :is-selected (= selected id) - :is-draggable is-draggable - :on-select on-select - :tree-path path - :tree-depth depth - :tree-index index - :tree-parent-path parent-path - :on-toggle on-toggle-set - :edition-id edition-id - :on-start-edition on-start-edition - :on-drop on-drop - :on-reset-edition on-reset-edition - :on-edit-submit on-edit-submit-set}])))) - -(mf/defc controlled-sets-list* - {::mf/props :obj} - [{:keys [token-sets - selected - on-update-token-set - on-update-token-set-group - is-token-set-active - is-token-set-group-active - on-create-token-set - on-toggle-token-set - on-toggle-token-set-group - on-start-edition - on-reset-edition - origin - on-select - new-path - edition-id]}] - - (assert (fn? is-token-set-group-active) "expected a function for `is-token-set-group-active` prop") - (assert (fn? is-token-set-active) "expected a function for `is-token-set-active` prop") - - (let [theme-modal? (= origin "theme-modal") - can-edit? (mf/use-ctx ctx/can-edit?) - draggable? (and (not theme-modal?) can-edit?) - empty-state? (and theme-modal? - (empty? token-sets) - (not new-path)) - - ;; NOTE: on-reset-edition and on-start-edition function can - ;; come as nil, in this case we need to provide a safe - ;; fallback for them - on-reset-edition - (mf/use-fn - (mf/deps on-reset-edition) - (fn [v] - (when (fn? on-reset-edition) - (on-reset-edition v)))) - - on-start-edition - (mf/use-fn - (mf/deps on-start-edition) - (fn [v] - (when (fn? on-start-edition) - (on-start-edition v))))] - - [:div {:class (stl/css :sets-list)} - (if ^boolean empty-state? - [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message-sets)} - (tr "workspace.tokens.no-sets-create")] - - [:> token-sets-tree* - {:is-draggable draggable? - :new-path new-path - :edition-id edition-id - :token-sets token-sets - :selected selected - :on-select on-select - :is-token-set-active is-token-set-active - :is-token-set-group-active is-token-set-group-active - :on-toggle-set on-toggle-token-set - :on-toggle-set-group on-toggle-token-set-group - :on-create-token-set on-create-token-set - :on-start-edition on-start-edition - :on-reset-edition on-reset-edition - :on-edit-submit-set on-update-token-set - :on-edit-submit-group on-update-token-set-group}])])) - (mf/defc sets-list* [{:keys [tokens-lib selected new-path edition-id]}] @@ -589,6 +82,6 @@ :on-toggle-token-set on-toggle-token-set-click :on-toggle-token-set-group on-toggle-token-set-group-click - :on-update-token-set on-update-token-set - :on-update-token-set-group on-update-token-set-group - :on-create-token-set on-create-token-set}])) + :on-update-token-set sets-helpers/on-update-token-set + :on-update-token-set-group sets-helpers/on-update-token-set-group + :on-create-token-set sets-helpers/on-create-token-set}])) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.cljs similarity index 98% rename from frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs rename to frontend/src/app/main/ui/workspace/tokens/sets/context_menu.cljs index e6975218f5..00f0e47e86 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.main.ui.workspace.tokens.sets-context-menu +(ns app.main.ui.workspace.tokens.sets.context-menu (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] diff --git a/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss similarity index 95% rename from frontend/src/app/main/ui/workspace/tokens/sets_context_menu.scss rename to frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss index f5b278499f..12438545c8 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss @@ -4,7 +4,7 @@ // // Copyright (c) KALEIDOS INC -@use "../../ds/typography.scss" as t; +@use "../../../ds/typography.scss" as t; @import "refactor/common-refactor.scss"; .token-set-context-menu { diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs b/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs new file mode 100644 index 0000000000..c05e2517ed --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs @@ -0,0 +1,36 @@ +(ns app.main.ui.workspace.tokens.sets.helpers + (:require + [app.common.types.tokens-lib :as ctob] + [app.main.data.event :as ev] + [app.main.data.workspace.tokens.library-edit :as dwtl] + [app.main.store :as st] + [potok.v2.core :as ptk])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HELPERS - Shared functions for token sets management +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn on-update-token-set + [token-set name] + (st/emit! (dwtl/clear-token-set-edition) + (dwtl/update-token-set token-set name))) + +(defn on-update-token-set-group + [path name] + (st/emit! (dwtl/clear-token-set-edition) + (dwtl/rename-token-set-group path name))) + +(defn on-create-token-set + [parent-set name] + (let [;; FIXME: this code should be reusable under helper under + ;; common types namespace + name + (if-let [parent-path (ctob/get-set-path parent-set)] + (->> (concat parent-path (ctob/split-set-name name)) + (ctob/join-set-path)) + (ctob/normalize-set-name name))] + + (st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name}) + (dwtl/create-token-set name)))) + + diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs b/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs new file mode 100644 index 0000000000..ef11226379 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs @@ -0,0 +1,492 @@ +;; 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.sets.lists + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.common.types.tokens-lib :as ctob] + [app.main.data.workspace.tokens.library-edit :as dwtl] + [app.main.store :as st] + [app.main.ui.context :as ctx] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.main.ui.hooks :as h] + [app.main.ui.workspace.tokens.sets.helpers :as sets-helpers] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [app.util.keyboard :as kbd] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn- on-start-creation + [] + (st/emit! (dwtl/start-token-set-creation []))) + +(defn- group-edition-id + "Prefix editing groups `edition-id` so it can be differentiated from sets with the same id." + [edition-id] + (str "group-" edition-id)) + + +(mf/defc editing-label* + {::mf/private true} + [{:keys [default-value on-cancel on-submit]}] + (let [on-submit + (mf/use-fn + (mf/deps on-cancel on-submit default-value) + (fn [event] + (let [value (dom/get-target-val event)] + (if (or (str/empty? value) + (= value default-value)) + (on-cancel) + (on-submit value))))) + + on-key-down + (mf/use-fn + (mf/deps on-submit on-cancel) + (fn [event] + (cond + (kbd/enter? event) (on-submit event) + (kbd/esc? event) (on-cancel))))] + [:input + {:class (stl/css :editing-node) + :type "text" + :on-blur on-submit + :on-key-down on-key-down + :maxlength "256" + :auto-focus true + :placeholder (tr "workspace.tokens.set-edit-placeholder") + :default-value default-value}])) + +(mf/defc checkbox* + [{:keys [checked aria-label on-click disabled]}] + (let [all? (true? checked) + mixed? (= checked "mixed") + checked? (or all? mixed?)] + + [:div {:role "checkbox" + :aria-checked (dm/str checked) + :disabled disabled + :title (when disabled (tr "workspace.tokens.no-permisions-set")) + :tab-index 0 + :class (stl/css-case :checkbox-style true + :checkbox-checked-style checked? + :checkbox-disabled-checked (and checked? disabled) + :checkbox-disabled disabled) + :on-click (when-not disabled on-click)} + + (when ^boolean checked? + [:> icon* + {:aria-label aria-label + :class (stl/css :check-icon) + :size "s" + :icon-id (if mixed? ic/remove ic/tick)}])])) + +(mf/defc inline-add-button* + [] + (let [can-edit? (mf/use-ctx ctx/can-edit?)] + (if can-edit? + [:div {:class (stl/css :empty-sets-wrapper)} + [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} + (tr "workspace.tokens.no-sets-yet")] + [:button {:on-click on-start-creation + :class (stl/css :create-set-button)} + (tr "workspace.tokens.create-one")]] + [:div {:class (stl/css :empty-sets-wrapper)} + [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} + (tr "workspace.tokens.no-sets-yet")]]))) + +(mf/defc add-button* + [] + [:> icon-button* {:variant "ghost" + :icon "add" + :on-click on-start-creation + :aria-label (tr "workspace.tokens.add set")}]) + +(mf/defc sets-tree-set-group* + {::mf/private true} + [{:keys [id label tree-depth tree-path is-active is-selected is-draggable is-collapsed tree-index on-drop + on-toggle-collapse on-toggle is-editing on-start-edition on-reset-edition on-edit-submit]}] + + (let [can-edit? + (mf/use-ctx ctx/can-edit?) + + label-id + (str id "-label") + + on-context-menu + (mf/use-fn + (mf/deps is-editing id tree-path can-edit?) + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (when (and can-edit? (not is-editing)) + (st/emit! (dwtl/assign-token-set-context-menu + {:position (dom/get-client-position event) + :is-group true + :id id + :edition-id (group-edition-id id) + :path tree-path}))))) + + on-collapse-click + (mf/use-fn + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (on-toggle-collapse tree-path))) + + on-double-click + (mf/use-fn (mf/deps id) #(on-start-edition (group-edition-id id))) + + on-checkbox-click + (mf/use-fn + (mf/deps on-toggle tree-path can-edit?) + #(on-toggle tree-path)) + + on-edit-submit' + (mf/use-fn + (mf/deps tree-path on-edit-submit can-edit?) + #(on-edit-submit tree-path %)) + + on-drop + (mf/use-fn + (mf/deps tree-index on-drop) + (fn [position data] + (on-drop tree-index position data))) + + [dprops dref] + (h/use-sortable + :data-type "penpot/token-set" + :on-drop on-drop + :data {:index tree-index + :is-group true} + :detect-center? true + :draggable? is-draggable)] + + [:div {:ref dref + :data-testid "tokens-set-group-item" + :style {"--tree-depth" tree-depth} + :class (stl/css-case :set-item-container true + :set-item-group true + :selected-set is-selected + :dnd-over (= (:over dprops) :center) + :dnd-over-top (= (:over dprops) :top) + :dnd-over-bot (= (:over dprops) :bot)) + :on-context-menu on-context-menu} + [:> icon-button* + {:class (stl/css :set-item-group-collapse-button) + :on-click on-collapse-click + :data-testid "tokens-set-group-collapse" + :aria-label (tr "labels.collapse") + :icon (if is-collapsed "arrow-right" "arrow-down") + :variant "action"}] + (if is-editing + [:> editing-label* + {:default-value label + :on-cancel on-reset-edition + :on-submit on-edit-submit'}] + [:* + [:div {:class (stl/css :set-name) + :role "button" + :title label + :tab-index 0 + :on-double-click on-double-click + :id label-id} + label] + [:> checkbox* + {:on-click on-checkbox-click + :disabled (not can-edit?) + :checked (case is-active + :all true + :partial "mixed" + :none false) + :arial-label (tr "workspace.tokens.select-set")}]])])) + +(mf/defc sets-tree-set* + [{:keys [id set label tree-depth tree-path tree-index is-selected is-active is-draggable is-editing + on-select on-drop on-toggle on-start-edition on-reset-edition on-edit-submit]}] + + (let [set-name (get set :name) + can-edit? (mf/use-ctx ctx/can-edit?) + + on-click + (mf/use-fn + (mf/deps is-editing tree-path) + (fn [event] + (dom/stop-propagation event) + (when-not is-editing + (when (fn? on-select) + (on-select set-name))))) + + on-context-menu + (mf/use-fn + (mf/deps is-editing id tree-path can-edit?) + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (when (and can-edit? (not is-editing)) + (st/emit! (dwtl/assign-token-set-context-menu + {:position (dom/get-client-position event) + :is-group false + :id id + :edition-id id + :path tree-path}))))) + + on-double-click + (mf/use-fn (mf/deps id) #(on-start-edition id)) + + on-checkbox-click + (mf/use-fn + (mf/deps set-name on-toggle) + (fn [event] + (dom/stop-propagation event) + (when (fn? on-toggle) + (on-toggle set-name)))) + + on-edit-submit' + (mf/use-fn + (mf/deps set on-edit-submit) + #(on-edit-submit set %)) + + on-drag + (mf/use-fn + (mf/deps tree-path) + (fn [_] + (when-not is-selected + (on-select tree-path)))) + + on-drop + (mf/use-fn + (mf/deps tree-index on-drop) + (fn [position data] + (on-drop tree-index position data))) + + [dprops dref] + (h/use-sortable + :data-type "penpot/token-set" + :on-drag on-drag + :on-drop on-drop + :data {:index tree-index + :is-group false} + :draggable? is-draggable) + + drop-over + (get dprops :over)] + + [:div {:ref dref + :role "button" + :data-testid "tokens-set-item" + :style {"--tree-depth" tree-depth} + :class (stl/css-case :set-item-container true + :selected-set is-selected + :dnd-over (= drop-over :center) + :dnd-over-top (= drop-over :top) + :dnd-over-bot (= drop-over :bot)) + :on-click on-click + :on-double-click on-double-click + :on-context-menu on-context-menu + :aria-checked is-active} + + [:> icon* + {:icon-id "document" + :class (stl/css-case :icon true + :root-icon (not tree-depth))}] + (if is-editing + [:> editing-label* + {:default-value label + :on-cancel on-reset-edition + :on-submit on-edit-submit'}] + [:* + [:div {:class (stl/css :set-name)} + label] + [:> checkbox* + {:on-click on-checkbox-click + :disabled (not can-edit?) + :arial-label (tr "workspace.tokens.select-set") + :checked is-active}]])])) + +(mf/defc token-sets-tree* + [{:keys [is-draggable + selected + is-token-set-group-active + is-token-set-active + on-start-edition + on-reset-edition + on-edit-submit-set + on-edit-submit-group + on-select + on-toggle-set + on-toggle-set-group + token-sets + new-path + edition-id]}] + + (let [collapsed-paths* (mf/use-state #{}) + collapsed-paths (deref collapsed-paths*) + + collapsed? + (mf/use-fn + (mf/deps collapsed-paths) + (partial contains? collapsed-paths)) + + on-drop + (mf/use-fn + (mf/deps collapsed-paths) + (fn [tree-index position data] + (let [params {:from-index (:index data) + :to-index tree-index + :position position + :collapsed-paths collapsed-paths}] + (if (:is-group data) + (st/emit! (dwtl/drop-token-set-group params)) + (st/emit! (dwtl/drop-token-set params)))))) + + on-toggle-collapse + (mf/use-fn + (fn [path] + (swap! collapsed-paths* #(if (contains? % path) + (disj % path) + (conj % path)))))] + + (for [{:keys [id token-set index is-new is-group path parent-path depth] :as node} + (ctob/sets-tree-seq token-sets + {:skip-children-pred collapsed? + :new-at-path new-path})] + (cond + ^boolean is-group + [:> sets-tree-set-group* + {:key index + :label (peek path) + :id id + :is-active (is-token-set-group-active path) + :is-selected false + :is-draggable is-draggable + :is-editing (= edition-id (group-edition-id id)) + :is-collapsed (collapsed? path) + :on-select on-select + + :tree-path path + :tree-depth depth + :tree-index index + :tree-parent-path parent-path + + :on-drop on-drop + :on-start-edition on-start-edition + :on-reset-edition on-reset-edition + :on-edit-submit on-edit-submit-group + :on-toggle-collapse on-toggle-collapse + :on-toggle on-toggle-set-group}] + + ^boolean is-new + [:> sets-tree-set* + {:key index + :set token-set + :label "" + :id id + :is-editing true + :is-active true + :is-selected true + + :tree-path path + :tree-depth depth + :tree-index index + :tree-parent-path parent-path + + :on-drop on-drop + :on-reset-edition on-reset-edition + :on-edit-submit sets-helpers/on-create-token-set}] + + :else + [:> sets-tree-set* + {:key index + :set token-set + :id id + :label (peek path) + :is-editing (= edition-id id) + :is-active (is-token-set-active id) + :is-selected (= selected id) + :is-draggable is-draggable + :on-select on-select + :tree-path path + :tree-depth depth + :tree-index index + :tree-parent-path parent-path + :on-toggle on-toggle-set + :edition-id edition-id + :on-start-edition on-start-edition + :on-drop on-drop + :on-reset-edition on-reset-edition + :on-edit-submit on-edit-submit-set}])))) + +(mf/defc controlled-sets-list* + {::mf/props :obj} + [{:keys [token-sets + selected + on-update-token-set + on-update-token-set-group + is-token-set-active + is-token-set-group-active + on-create-token-set + on-toggle-token-set + on-toggle-token-set-group + on-start-edition + on-reset-edition + origin + on-select + new-path + edition-id]}] + + (assert (fn? is-token-set-group-active) "expected a function for `is-token-set-group-active` prop") + (assert (fn? is-token-set-active) "expected a function for `is-token-set-active` prop") + + (let [theme-modal? (= origin "theme-modal") + can-edit? (mf/use-ctx ctx/can-edit?) + draggable? (and (not theme-modal?) can-edit?) + empty-state? (and theme-modal? + (empty? token-sets) + (not new-path)) + + ;; NOTE: on-reset-edition and on-start-edition function can + ;; come as nil, in this case we need to provide a safe + ;; fallback for them + on-reset-edition + (mf/use-fn + (mf/deps on-reset-edition) + (fn [v] + (when (fn? on-reset-edition) + (on-reset-edition v)))) + + on-start-edition + (mf/use-fn + (mf/deps on-start-edition) + (fn [v] + (when (fn? on-start-edition) + (on-start-edition v))))] + + [:div {:class (stl/css :sets-list)} + (if ^boolean empty-state? + [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message-sets)} + (tr "workspace.tokens.no-sets-create")] + + [:> token-sets-tree* + {:is-draggable draggable? + :new-path new-path + :edition-id edition-id + :token-sets token-sets + :selected selected + :on-select on-select + :is-token-set-active is-token-set-active + :is-token-set-group-active is-token-set-group-active + :on-toggle-set on-toggle-token-set + :on-toggle-set-group on-toggle-token-set-group + :on-create-token-set on-create-token-set + :on-start-edition on-start-edition + :on-reset-edition on-reset-edition + :on-edit-submit-set on-update-token-set + :on-edit-submit-group on-update-token-set-group}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss b/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss new file mode 100644 index 0000000000..f0ba86f54d --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss @@ -0,0 +1,164 @@ +// 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 + +@use "../../../ds/typography.scss" as *; +@import "refactor/common-refactor.scss"; + +.sets-list { + width: 100%; + margin-bottom: 0; + overflow-y: auto; +} + +.empty-sets-wrapper { + padding: $s-12; + padding-inline-start: $s-24; + color: var(--color-foreground-secondary); +} + +.create-set-button { + @include use-typography("body-small"); + background-color: transparent; + border: none; + appearance: none; + color: var(--color-accent-primary); + cursor: pointer; +} + +.set-item-container { + @include bodySmallTypography; + display: flex; + align-items: center; + width: 100%; + min-height: $s-32; + cursor: pointer; + color: var(--layer-row-foreground-color); + padding-left: calc($s-24 * var(--tree-depth, 0) + $s-8); + border: $s-2 solid transparent; + gap: $s-2; + + &.dnd-over-bot { + border-bottom: $s-2 solid var(--layer-row-foreground-color-hover); + } + &.dnd-over-top { + border-top: $s-2 solid var(--layer-row-foreground-color-hover); + } + &.dnd-over { + border: $s-2 solid var(--layer-row-foreground-color-hover); + } +} + +.set-item-group { + cursor: unset; + padding-left: calc($s-24 * var(--tree-depth, 0)); + gap: 0; +} + +.set-item-group-collapse-button { + cursor: pointer; + width: auto; + height: $s-28; +} + +.set-name { + @include textEllipsis; + flex-grow: 1; + padding-left: $s-2; +} + +.icon { + flex-shrink: 0; + display: flex; + align-items: center; + width: $s-20; + height: $s-20; + padding-right: $s-4; +} + +.root-icon { + margin-left: $s-8; +} + +.checkbox-style { + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + width: $s-16; + height: $s-16; + margin-inline: $s-6; + background-color: var(--input-checkbox-background-color-rest); + border: $s-1 solid var(--input-checkbox-border-color-rest); + border-radius: $s-4; + padding: 0; +} + +.checkbox-checked-style { + background-color: var(--input-border-color-active); + color: var(--color-background-secondary); +} + +.checkbox-disabled { + border: $s-1 solid var(--color-background-quaternary); + background-color: var(--color-background-tertiary); +} + +.checkbox-disabled-checked { + background-color: var(--color-accent-primary-muted); + color: var(--color-background-quaternary); +} + +.check-icon { + color: currentColor; +} + +.set-item-container:hover { + background-color: var(--layer-row-background-color-hover); + color: var(--layer-row-foreground-color-hover); + box-shadow: -100px 0 0 0 var(--layer-row-background-color-hover); +} + +.empty-state-message-sets { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + padding: $s-12; + color: var(--color-foreground-secondary); +} +.selected-set { + background-color: var(--layer-row-background-color-selected); + color: var(--layer-row-foreground-color-selected); + box-shadow: -100px 0 0 0 var(--layer-row-background-color-selected); +} + +.collapsabled-icon { + @include buttonStyle; + @include flexCenter; + height: $s-24; + border-radius: $br-8; + &:hover { + color: var(--title-foreground-color-hover); + } +} + +.editing-node { + @include textEllipsis; + @include bodySmallTypography; + @include removeInputStyle; + + border: $s-1 solid var(--input-border-color-focus); + border-radius: $br-8; + color: var(--layer-row-foreground-color-focus); + flex-grow: 1; + height: $s-28; + margin: 0; + padding-left: $s-6; + + &::placeholder { + color: var(--layer-row-foreground-color-placeholder); + } +} diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 26b06241c2..a9aaa8885d 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -29,7 +29,8 @@ [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.main.ui.workspace.tokens.common.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.sets :as tsets] - [app.main.ui.workspace.tokens.sets-context-menu :refer [token-set-context-menu*]] + [app.main.ui.workspace.tokens.sets.context-menu :refer [token-set-context-menu*]] + [app.main.ui.workspace.tokens.sets.lists :as tsetslist] [app.main.ui.workspace.tokens.themes :refer [themes-header*]] [app.main.ui.workspace.tokens.token-pill :refer [token-pill*]] [app.util.array :as array] @@ -188,7 +189,7 @@ (not token-set-new-path)) (when-not token-set-new-path - [:> tsets/inline-add-button*]) + [:> tsetslist/inline-add-button*]) [:> h/sortable-container {} [:> tsets/sets-list* @@ -197,7 +198,7 @@ :edition-id token-set-edition-id :selected selected-token-set-name}]]))) -(mf/defc token-sets-section* +(mf/defc token-management-section* {::mf/private true} [{:keys [resize-height] :as props}] @@ -206,17 +207,16 @@ [:* [:> token-set-context-menu*] - [:article {:data-testid "token-themes-sets-sidebar" - :class (stl/css :sets-section-wrapper) + [:section {:data-testid "token-management-sidebar" + :class (stl/css :token-management-section-wrapper) :style {"--resize-height" (str resize-height "px")}} - [:div {:class (stl/css :sets-sidebar)} - [:> themes-header*] - [:div {:class (stl/css :sidebar-header)} - [:& title-bar {:title (tr "labels.sets")} - (when can-edit? - [:> tsets/add-button*])]] + [:> themes-header*] + [:div {:class (stl/css :sidebar-header)} + [:& title-bar {:title (tr "labels.sets")} + (when can-edit? + [:> tsetslist/add-button*])]] - [:> token-sets-list* props]]]])) + [:> token-sets-list* props]]])) (mf/defc tokens-section* [{:keys [tokens-lib]}] @@ -398,7 +398,7 @@ (mf/deref refs/tokens-lib)] [:div {:class (stl/css :sidebar-wrapper)} - [:> token-sets-section* + [:> token-management-section* {:resize-height size-pages-opened :tokens-lib tokens-lib}] [:article {:class (stl/css :tokens-section-wrapper) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index d8e2c14370..a234852628 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -17,7 +17,7 @@ overflow: hidden; } -.sets-section-wrapper { +.token-management-section-wrapper { position: relative; display: flex; flex: 1; @@ -25,6 +25,8 @@ flex-direction: column; overflow-y: auto; scrollbar-gutter: stable; + position: relative; + padding-block-end: var(--sp-l); } .tokens-section-wrapper { @@ -34,11 +36,6 @@ scrollbar-gutter: stable; } -.sets-sidebar { - position: relative; - padding-block-end: var(--sp-l); -} - .sets-header-container { @include use-typography("headline-small"); padding: var(--sp-s); diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs index d7c0934a98..570b9f97e2 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs @@ -26,7 +26,7 @@ [app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.icons :as i] - [app.main.ui.workspace.tokens.sets :as wts] + [app.main.ui.workspace.tokens.sets.lists :as wts] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.keyboard :as k]