diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs index 770decba9..47ea49ab7 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs @@ -306,7 +306,7 @@ title] (when children [:* - [:> icon* {:icon-d "arrow" :size "s"}] + [:> icon* {:icon-id "arrow" :size "s"}] [:ul {:class (stl/css :token-context-submenu) :data-direction submenu-direction :ref submenu-ref diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index c36c4f4b8..72b912243 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -73,16 +73,27 @@ :default-value default-value}])) (mf/defc checkbox - [{:keys [checked aria-label on-click]}] + [{:keys [checked aria-label on-click disabled]}] (let [all? (true? checked) mixed? (= checked "mixed") - checked? (or all? mixed?)] + checked? (or all? mixed?) + on-click + (mf/use-fn + (mf/deps disabled) + (fn [e] + (when-not disabled + (on-click e))))] [:div {:role "checkbox" :aria-checked (dm/str checked) + :disabled disabled + :title (when disabled (tr "workspace.token.no-permisions-set")) :tab-index 0 :class (stl/css-case :checkbox-style true - :checkbox-checked-style checked?) + :checkbox-checked-style checked? + :checkbox-disabled-checked (and checked? disabled) + :checkbox-disabled disabled) :on-click on-click} + (when checked? [:> icon* {:aria-label aria-label @@ -94,13 +105,14 @@ [{:keys [label tree-depth tree-path active? selected? collapsed? editing? on-toggle on-edit on-edit-reset on-edit-submit]}] (let [editing?' (editing? tree-path) active?' (active? tree-path) + can-edit? (:can-edit (deref refs/permissions)) on-context-menu (mf/use-fn - (mf/deps editing? tree-path) + (mf/deps editing? tree-path can-edit?) (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (when-not (editing? tree-path) + (when (and can-edit? (not editing?')) (st/emit! (wdt/show-token-set-context-menu {:position (dom/get-client-position event) @@ -110,18 +122,26 @@ (fn [event] (dom/stop-propagation event) (swap! collapsed? not))) + on-double-click (mf/use-fn - (mf/deps tree-path) - #(on-edit tree-path)) + (mf/deps tree-path can-edit?) + (fn [] + (when can-edit? + (on-edit tree-path)))) + on-checkbox-click (mf/use-fn - (mf/deps on-toggle tree-path) - #(on-toggle tree-path)) + (mf/deps on-toggle tree-path can-edit?) + (fn [] + (when can-edit? + (on-toggle tree-path)))) + on-edit-submit' (mf/use-fn - (mf/deps tree-path on-edit-submit) - #(on-edit-submit tree-path %))] + (mf/deps tree-path on-edit-submit can-edit?) + (fn [e] + (when can-edit? (on-edit-submit tree-path e))))] [:div {:role "button" :data-testid "tokens-set-group-item" :style {"--tree-depth" tree-depth} @@ -147,6 +167,7 @@ label] [:& checkbox {:on-click on-checkbox-click + :disabled (not can-edit?) :checked (case active?' :all true :partial "mixed" @@ -158,6 +179,7 @@ (let [set-name (.-name set) editing?' (editing? tree-path) active?' (some? (active? set-name)) + can-edit? (:can-edit (deref refs/permissions)) on-click (mf/use-fn (mf/deps editing?' tree-path) @@ -167,11 +189,11 @@ (on-select tree-path)))) on-context-menu (mf/use-fn - (mf/deps editing?' tree-path) + (mf/deps editing?' tree-path can-edit?) (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (when-not editing?' + (when (and can-edit? (not editing?')) (st/emit! (wdt/show-token-set-context-menu {:position (dom/get-client-position event) @@ -211,6 +233,7 @@ label] [:& checkbox {:on-click on-checkbox-click + :disabled (not can-edit?) :arial-label (tr "workspace.token.select-set") :checked active?'}]])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.scss b/frontend/src/app/main/ui/workspace/tokens/sets.scss index a0a84192f..357fa9d53 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets.scss @@ -68,17 +68,28 @@ height: $s-16; margin-inline: $s-6; background-color: var(--input-checkbox-background-color-rest); - border: 1px solid var(--input-checkbox-border-color-rest); - border-radius: 0.25rem; + 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: var(--color-background-secondary); + color: currentColor; } .set-item-container:hover { diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 03b555b18..1fc420ee2 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -112,18 +112,20 @@ (wtch/toggle-token {:token token :shapes selected-shapes :token-type-props token-type-props}))))) - tokens-count (count tokens)] + tokens-count (count tokens) + can-edit? (:can-edit (deref refs/permissions))] [:div {:on-click on-toggle-open-click} [:& cmm/asset-section {:icon (token-section-icon type) :title title :assets-count tokens-count :open? open?} [:& cmm/asset-section-block {:role :title-button} - [:> icon-button* {:on-click on-popover-open-click - :variant "ghost" - :icon "add" - ;; TODO: This needs translation - :aria-label (str "Add token: " title)}]] + (when can-edit? + [:> icon-button* {:on-click on-popover-open-click + :variant "ghost" + :icon "add" + ;; TODO: This needs translation + :aria-label (str "Add token: " title)}])] (when open? [:& cmm/asset-section-block {:role :content} [:div {:class (stl/css :token-pills-wrapper)} @@ -137,7 +139,9 @@ on-context-menu (fn [e] (on-context-menu e token))] [:& token-pill {:key (:name token) + :token-type-props token-type-props :token token + :selected-shapes selected-shapes :theme-token theme-token :half-applied (or (and applied multiple-selection) (and applied (not full-applied))) @@ -165,6 +169,7 @@ (mf/defc themes-header [_props] (let [ordered-themes (mf/deref refs/workspace-token-themes-no-hidden) + can-edit? (:can-edit (deref refs/permissions)) open-modal (mf/use-fn (fn [e] @@ -176,34 +181,45 @@ [:div {:class (stl/css :empty-theme-wrapper)} [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} (tr "workspace.token.no-themes")] - [:button {:on-click open-modal - :class (stl/css :create-theme-button)} - (tr "workspace.token.create-one")]] - [:div {:class (stl/css :theme-select-wrapper)} - [:& theme-select] - [:> button* {:variant "secondary" - :class (stl/css :edit-theme-button) - :on-click open-modal} - (tr "labels.edit")]])])) + (when can-edit? + [:button {:on-click open-modal + :class (stl/css :create-theme-button)} + (tr "workspace.token.create-one")])] + (if can-edit? + [:div {:class (stl/css :theme-select-wrapper)} + [:& theme-select] + [:> button* {:variant "secondary" + :class (stl/css :edit-theme-button) + :on-click open-modal} + (tr "labels.edit")]] + [:div {:title (when-not can-edit? + (tr "workspace.token.no-permission-themes"))} + [:& theme-select]]))])) (mf/defc add-set-button [{:keys [on-open style]}] (let [{:keys [on-create new?]} (sets-context/use-context) on-click #(do (on-open) - (on-create))] + (on-create)) + can-edit? (:can-edit (deref refs/permissions))] (if (= style "inline") (when-not new? - [:div {:class (stl/css :empty-sets-wrapper)} - [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} - (tr "workspace.token.no-sets-yet")] - [:button {:on-click on-click - :class (stl/css :create-theme-button)} - (tr "workspace.token.create-one")]]) - [:> icon-button* {:variant "ghost" - :icon "add" - :on-click on-click - :aria-label (tr "workspace.token.add set")}]))) + (if can-edit? + [:div {:class (stl/css :empty-sets-wrapper)} + [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} + (tr "workspace.token.no-sets-yet")] + [:button {:on-click on-click + :class (stl/css :create-theme-button)} + (tr "workspace.token.create-one")]] + [:div {:class (stl/css :empty-sets-wrapper)} + [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} + (tr "workspace.token.no-sets-yet")]])) + (when can-edit? + [:> icon-button* {:variant "ghost" + :icon "add" + :on-click on-click + :aria-label (tr "workspace.token.add set")}])))) (mf/defc theme-sets-list [{:keys [on-open]}] @@ -219,7 +235,8 @@ (mf/defc themes-sets-tab [{:keys [resize-height]}] (let [open? (mf/use-state true) - on-open (mf/use-fn #(reset! open? true))] + on-open (mf/use-fn #(reset! open? true)) + can-edit? (:can-edit (deref refs/permissions))] [:& sets-context/provider {} [:& sets-context-menu] [:article {:data-testid "token-themes-sets-sidebar" @@ -229,8 +246,9 @@ [:& themes-header] [:div {:class (stl/css :sidebar-header)} [:& title-bar {:title (tr "labels.sets")} - [:& add-set-button {:on-open on-open - :style "header"}]]] + (when can-edit? + [:& add-set-button {:on-open on-open + :style "header"}])]] [:& theme-sets-list {:on-open on-open}]]]])) (mf/defc tokens-tab @@ -270,6 +288,7 @@ [{:keys []}] (let [show-menu* (mf/use-state false) show-menu? (deref show-menu*) + can-edit? (:can-edit (deref refs/permissions)) open-menu (mf/use-fn @@ -311,12 +330,13 @@ (dom/trigger-download "tokens.json"))))] [:div {:class (stl/css :import-export-button-wrapper)} - [:input {:type "file" - :ref input-ref - :style {:display "none"} - :id "file-input" - :accept ".json" - :on-change on-import}] + (when can-edit? + [:input {:type "file" + :ref input-ref + :style {:display "none"} + :id "file-input" + :accept ".json" + :on-change on-import}]) [:> button* {:on-click open-menu :icon "import-export" :variant "secondary"} @@ -324,9 +344,10 @@ [:& dropdown-menu {:show show-menu? :on-close close-menu :list-class (stl/css :import-export-menu)} - [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) - :on-click on-option-click} - (tr "labels.import")] + (when can-edit? + [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) + :on-click on-option-click} + (tr "labels.import")]) [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) :on-click on-export} diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index 1241b2a67..070f8470e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -65,7 +65,7 @@ display: flex; align-items: center; justify-content: space-between; - margin-left: $s-8; + margin-left: $s-12; padding-top: $s-12; color: var(--layer-row-foreground-color); } diff --git a/frontend/src/app/main/ui/workspace/tokens/theme_select.cljs b/frontend/src/app/main/ui/workspace/tokens/theme_select.cljs index 85a3e2eeb..397bbb70b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/theme_select.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/theme_select.cljs @@ -78,7 +78,7 @@ active-theme-paths (mf/deref refs/workspace-active-theme-paths-no-hidden) active-themes-count (count active-theme-paths) themes (mf/deref refs/workspace-token-theme-tree-no-hidden) - + can-edit? (:can-edit (deref refs/permissions)) ;; Data current-label (cond (> active-themes-count 1) (tr "workspace.token.active-themes" active-themes-count) @@ -97,15 +97,23 @@ ;; 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))] + + on-open-dropdown + (mf/use-fn + (mf/deps can-edit?) + (fn [] + (when can-edit? + (swap! state* assoc :is-open? true))))] ;; TODO: This element should be accessible by keyboard [:div {:on-click on-open-dropdown + :disabled (not can-edit?) :aria-expanded is-open? :aria-haspopup "listbox" :tab-index "0" :role "combobox" - :class (stl/css :custom-select)} + :class (stl/css-case :custom-select true + :disabled-select (not can-edit?))} [:> text* {:as "span" :typography "body-small" :class (stl/css :current-label)} current-label] [:> icon* {:icon-id i/arrow-down :class (stl/css :dropdown-button) :aria-hidden true}] diff --git a/frontend/src/app/main/ui/workspace/tokens/theme_select.scss b/frontend/src/app/main/ui/workspace/tokens/theme_select.scss index 79c0f3fc2..297b95f1f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/theme_select.scss +++ b/frontend/src/app/main/ui/workspace/tokens/theme_select.scss @@ -47,7 +47,7 @@ color: var(--color-foreground-secondary); } -.disabled { +.disabled-select { --custom-select-bg-color: var(--menu-background-color-disabled); --custom-select-border-color: var(--menu-border-color-disabled); --custom-select-icon-color: var(--menu-foreground-color-disabled); diff --git a/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs b/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs index 4c6d3910b..14cbb273d 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs @@ -1,8 +1,11 @@ (ns app.main.ui.workspace.tokens.token-pill - (:require-macros [app.main.style :as stl]) + (:require-macros + [app.common.data.macros :as dm] + [app.main.style :as stl]) (:require [app.common.files.helpers :as cfh] [app.common.types.tokens-lib :as ctob] + [app.main.refs :as refs] [app.main.ui.components.color-bullet :refer [color-bullet]] [app.main.ui.ds.foundations.assets.icon :refer [icon*]] [app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]] @@ -13,16 +16,135 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) +;; Translation dictionaries +(def ^:private attribute-dictionary + {:rotation "Rotation" + :opacity "Opacity" + :stroke-width "Stroke Width" + + ;; Spacing + :p1 "Top" :p2 "Right" :p3 "Bottom" :p4 "Left" + :column-gap "Column Gap" :row-gap "Row Gap" + + ;; Sizing + :width "Width" + :height "Height" + :layout-item-min-w "Min Width" + :layout-item-min-h "Min Height" + :layout-item-max-w "Max Width" + :layout-item-max-h "Max Height" + + ;; Border Radius + :r1 "Top Left" :r2 "Top Right" :r4 "Bottom Left" :r3 "Bottom Right" + + ;; Dimensions + :x "X" :y "Y" + + ;; Color + :fill "Fill" + :stroke-color "Stroke Color"}) + +(def ^:private dimensions-dictionary + {:stroke-width :stroke-width + :p1 :spacing + :p2 :spacing + :p3 :spacing + :p4 :spacing + :column-gap :spacing + :row-gap :spacing + :width :sizing + :height :sizing + :layout-item-min-w :sizing + :layout-item-min-h :sizing + :layout-item-max-w :sizing + :layout-item-max-h :sizing + :r1 :border-radius + :r2 :border-radius + :r4 :border-radius + :r3 :border-radius + :x :x + :y :y}) + +(def ^:private category-dictionary + {:stroke-width "Stroke Width" + :spacing "Spacing" + :sizing "Sizing" + :border-radius "Border Radius" + :x "X" + :y "Y"}) + +;; Helper functions +(defn partially-applied-attr + "Translates partially applied attributes based on the dictionary." + [app-token-keys is-applied token-type-props] + (let [{:keys [attributes all-attributes]} token-type-props + filtered-keys (if all-attributes + (filter #(contains? all-attributes %) app-token-keys) + (filter #(contains? attributes %) app-token-keys))] + (when is-applied + (str/join ", " (map attribute-dictionary filtered-keys))))) + +(defn translate-and-format + "Translates and formats grouped values by category." + [grouped-values] + (str/join "\n" + (map (fn [[category values]] + (if (#{:x :y} category) + (dm/str "- " (category-dictionary category)) + (dm/str "- " (category-dictionary category) ": " + (str/join ", " (map attribute-dictionary values)) "."))) + grouped-values))) + +(defn token-pill-tooltip + "Generates a tooltip for a given token." + [theme-token is-viewer shape token-type-props token half-applied] + (let [{:keys [name value resolved-value errors type]} token + {:keys [title]} token-type-props + applied-tokens (:applied-tokens shape) + app-token-vals (set (vals applied-tokens)) + app-token-keys (keys applied-tokens) + is-applied? (contains? app-token-vals name) + no-token-active (nil? theme-token) + errors? (or no-token-active (seq errors)) + applied-to (if half-applied + (partially-applied-attr app-token-keys is-applied? token-type-props) + (tr "labels.all")) + grouped-values (group-by dimensions-dictionary app-token-keys) + + base-title (dm/str "Token: " name "\n" + (tr "workspace.token.original-value" value) "\n" + (tr "workspace.token.resolved-value" resolved-value))] + + (cond + ;; If there are errors, show the appropriate message + errors? (if no-token-active + (tr "workspace.token-set.not-active") + (sd/humanize-errors token)) + ;; If the token is applied and the user is a is-viewer, show the details + (and is-applied? is-viewer) + (->> [base-title + (tr "workspace.token.applied-to") + (if (= :dimensions type) + (translate-and-format grouped-values) + (str "- " title ": " applied-to))] + (str/join "\n")) + ;; Otherwise only show the base title + :else base-title))) + (mf/defc token-pill {::mf/wrap-props false} - [{:keys [on-click token theme-token full-applied on-context-menu half-applied]}] - (let [{:keys [name value resolved-value errors]} token - errors? (or (nil? theme-token) (and (seq errors) (seq (:errors theme-token)))) - color (when (seq (ctob/find-token-value-references value)) - (wtt/resolved-value-hex theme-token)) + [{:keys [on-click token theme-token full-applied on-context-menu half-applied selected-shapes token-type-props]}] + (let [{:keys [name value errors]} token + + can-edit? (:can-edit (deref refs/permissions)) + is-viewer (not can-edit?) + errors? (or (nil? theme-token) (and (seq errors) (seq (:errors theme-token)))) + color (when (seq (ctob/find-token-value-references value)) + (wtt/resolved-value-hex theme-token)) contains-path? (str/includes? name ".") splitted-name (cfh/split-by-last-period name) color (or color (wtt/resolved-value-hex token)) + on-click (mf/use-callback (mf/deps errors? on-click) @@ -37,21 +159,46 @@ full-applied "token-status-full" :else - "token-status-non-applied")] + "token-status-non-applied") + + on-context-menu + (mf/use-fn + (mf/deps can-edit? on-context-menu) + (fn [e] + (dom/stop-propagation e) + (when can-edit? + (on-context-menu e)))) + + on-click + (mf/use-fn + (mf/deps errors? on-click) + (fn [event] + (dom/stop-propagation event) + (when (and can-edit? (not (seq errors)) on-click) + (on-click event)))) + + on-hover + (mf/use-fn + (mf/deps selected-shapes is-viewer) + (fn [event] + (let [node (dom/get-current-target event) + title (token-pill-tooltip theme-token is-viewer (first selected-shapes) token-type-props token half-applied)] + (dom/set-attribute! node "title" title))))] + [:button {:class (stl/css-case :token-pill true - :token-pill-applied (or half-applied full-applied) - :token-pill-invalid errors? - :token-pill-invalid-applied (and full-applied errors?)) + :token-pill-applied (and can-edit? (or half-applied full-applied)) + :token-pill-invalid (and can-edit? errors?) + :token-pill-invalid-applied (and full-applied errors? can-edit?) + :token-pill-viewer is-viewer + :token-pill-applied-viewer (and is-viewer + (or half-applied full-applied)) + :token-pill-invalid-viewer (and is-viewer + errors?) + :token-pill-invalid-applied-viewer (and is-viewer + (and full-applied errors?))) :type "button" - :title (cond - errors? (if (nil? theme-token) - (tr "workspace.token-set.not-active") - (sd/humanize-errors token)) - :else (->> [(str "Token: " name) - (tr "workspace.token.original-value" value) - (tr "workspace.token.resolved-value" resolved-value)] - (str/join "\n"))) :on-click on-click + :on-mouse-enter on-hover :on-context-menu on-context-menu} (cond errors? diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 4b8b77f54..5c70ddfa3 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6723,6 +6723,10 @@ msgstr "Add set" msgid "workspace.token.tools" msgstr "Tools" +#: src/app/main/ui/workspace/tokens/sidebar.cljs +msgid "workspace.token.no-permission-themes" +msgstr "You need to be an editor to use themes" + #: src/app/main/ui/workspace/tokens/modals/themes.cljs msgid "workspace.token.save-theme" msgstr "Save theme" @@ -6799,6 +6803,10 @@ msgstr "Define what token sets should be used as part of this theme option:" msgid "workspace.token.no-sets-yet" msgstr "There are no sets yet." +#: src/app/main/ui/workspace/tokens/sets.cljs +msgid "workspace.token.no-permisions-set" +msgstr "You need to be an editor to activate / deactivate sets" + #: src/app/main/ui/workspace/tokens/sets.cljs msgid "workspace.token.no-sets-create" msgstr "There are no sets defined yet. Create one first." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 00f61a63c..2854a27e0 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -6729,6 +6729,10 @@ msgstr "Añadir set" msgid "workspace.token.tools" msgstr "Herramientas" +#: src/app/main/ui/workspace/tokens/sidebar.cljs +msgid "workspace.token.no-permission-themes" +msgstr "Debes ser editor para usar temas" + #: src/app/main/ui/workspace/tokens/modals/themes.cljs msgid "workspace.token.save-theme" msgstr "Guardar tema" @@ -6829,6 +6833,10 @@ msgstr "Duplicar token" msgid "workspace.token.edit" msgstr "Editar token" +#: src/app/main/ui/workspace/tokens/sets.cljs +msgid "workspace.token.no-permisions-set" +msgstr "Debes ser editor para activar / desactivar sets" + msgid "workspace.versions.button.save" msgstr "Guardar versión"