♻️ Restructure UI files for token sets

This commit is contained in:
Xavier Julian 2025-06-19 14:03:26 +02:00 committed by Xaviju
parent e258030bc0
commit ce59070fd1
10 changed files with 719 additions and 537 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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*])]]
[:> 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)

View file

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

View file

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