mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
✨ Add color token on colorpicker (#7197)
* ✨ Add token aplication to colorpicker * 🐛 Change fn name * 🐛 Change scss from file * 🐛 Change color for direct-color * 🐛 Remove vector from fns * 🐛 Fix CI * 🐛 Change color-option name * 🐛 Fix comments * 🐛 Remove sets without color tokens
This commit is contained in:
@@ -120,6 +120,7 @@
|
||||
:tiered-file-data-storage
|
||||
:token-units
|
||||
:token-base-font-size
|
||||
:token-color
|
||||
:token-typography-types
|
||||
:token-typography-composite
|
||||
:transit-readable-response
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
(f shape-ids {:color hex :opacity opacity} 0 {:ignore-touched true
|
||||
:page-id page-id}))))
|
||||
|
||||
(defn- value->color
|
||||
(defn value->color
|
||||
"Transform a token color value into penpot color data structure"
|
||||
[color]
|
||||
(when-let [tc (tinycolor/valid-color color)]
|
||||
@@ -584,6 +584,35 @@
|
||||
:shape-ids shape-ids
|
||||
:on-update-shape on-update-shape}))))))))
|
||||
|
||||
|
||||
(defn apply-token-on-selected
|
||||
[color-operations token]
|
||||
(ptk/reify ::apply-token-on-selected
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [undo-id (js/Symbol)]
|
||||
(rx/concat
|
||||
(rx/of (dwu/start-undo-transaction undo-id))
|
||||
(->> (rx/from color-operations)
|
||||
(rx/map
|
||||
(fn [cop]
|
||||
(let [shape-ids [(:shape-id cop)]]
|
||||
(case (:prop cop)
|
||||
:fill (apply-token {:attributes #{:fill}
|
||||
:token token
|
||||
:shape-ids shape-ids
|
||||
:on-update-shape update-fill})
|
||||
:stroke (apply-token {:attributes #{:stroke-color}
|
||||
:token token
|
||||
:shape-ids shape-ids
|
||||
:on-update-shape update-stroke-color})
|
||||
;; Text
|
||||
:content (apply-token {:attributes #{:fill}
|
||||
:token token
|
||||
:shape-ids shape-ids
|
||||
:on-update-shape update-fill})
|
||||
:shadow (rx/empty))))))
|
||||
(rx/of (dwu/commit-undo-transaction undo-id)))))))
|
||||
;; Map token types to different properties used along the cokde ---------------------------------------------
|
||||
|
||||
;; FIXME: the values should be lazy evaluated, probably a function,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.title-bar {
|
||||
@include headlineSmallTypography;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -20,7 +21,6 @@
|
||||
.title,
|
||||
.title-only,
|
||||
.inspect-title {
|
||||
@include headlineSmallTypography;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
@use "./utils.scss" as *;
|
||||
|
||||
// TODO: create actual tokens once we have them from design
|
||||
$sz-1: px2rem(1);
|
||||
$sz-6: px2rem(6);
|
||||
$sz-16: px2rem(16);
|
||||
$sz-24: px2rem(24);
|
||||
$sz-28: px2rem(28);
|
||||
$sz-32: px2rem(32);
|
||||
$sz-36: px2rem(36);
|
||||
$sz-40: px2rem(40);
|
||||
@@ -26,6 +28,7 @@ $sz-318: px2rem(318);
|
||||
$sz-352: px2rem(352);
|
||||
$sz-384: px2rem(384);
|
||||
$sz-400: px2rem(400);
|
||||
$sz-430: px2rem(430);
|
||||
$sz-480: px2rem(480);
|
||||
$sz-500: px2rem(500);
|
||||
$sz-964: px2rem(964);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
@use "../spacing.scss" as *;
|
||||
@use "../borders.scss" as *;
|
||||
@use "../sizes.scss" as *;
|
||||
@use "../typography.scss" as t;
|
||||
|
||||
.input-wrapper {
|
||||
--opacity-button: 0;
|
||||
|
||||
@@ -172,7 +172,9 @@
|
||||
[:id {:optional true} :string]
|
||||
[:offset {:optional true} :int]
|
||||
[:delay {:optional true} :int]
|
||||
[:content [:or fn? :string [:fn mf/element?]]]
|
||||
;; TODO: Review why html element crash schema
|
||||
;; https://tree.taiga.io/project/penpot/task/12039
|
||||
;; [:content [:or fn? :string [:fn mf/element?]]]
|
||||
[:placement {:optional true}
|
||||
[:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]])
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.color :as ct]
|
||||
[app.config :as cfg]
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
@@ -66,7 +67,7 @@
|
||||
|
||||
(mf/defc swatch*
|
||||
{::mf/schema (sm/schema schema:swatch)}
|
||||
[{:keys [background on-click size active class]
|
||||
[{:keys [background on-click size active class tooltip-content]
|
||||
:rest props}]
|
||||
(let [;; NOTE: this code is only relevant for storybook, because
|
||||
;; storybook is unable to pass in a comfortable way a complex
|
||||
@@ -90,6 +91,7 @@
|
||||
:stops gradient-stops}
|
||||
image (:image background)
|
||||
format (if id? "rounded" "square")
|
||||
element-id (mf/use-id)
|
||||
|
||||
class
|
||||
(dm/str class " " (stl/css-case
|
||||
@@ -106,23 +108,26 @@
|
||||
(mf/spread-props props {:class class
|
||||
:on-click on-click
|
||||
:type button-type
|
||||
:title (color-title background)})]
|
||||
:aria-labelledby element-id})]
|
||||
|
||||
[:> element-type props
|
||||
(cond
|
||||
[:> tooltip* {:content (if tooltip-content
|
||||
tooltip-content
|
||||
(color-title background))
|
||||
:id element-id}
|
||||
[:> element-type props
|
||||
(cond
|
||||
(some? gradient-type)
|
||||
[:span {:class (stl/css :swatch-gradient)
|
||||
:style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
|
||||
|
||||
(some? gradient-type)
|
||||
[:span {:class (stl/css :swatch-gradient)
|
||||
:style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
|
||||
(some? image)
|
||||
(let [uri (cfg/resolve-file-media image)]
|
||||
[:span {:class (stl/css :swatch-image)
|
||||
:style {:background-image (str/ffmt "url(%)" uri)}}])
|
||||
|
||||
(some? image)
|
||||
(let [uri (cfg/resolve-file-media image)]
|
||||
[:span {:class (stl/css :swatch-image)
|
||||
:style {:background-image (str/ffmt "url(%)" uri)}}])
|
||||
|
||||
:else
|
||||
[:span {:class (stl/css :swatch-opacity)}
|
||||
[:span {:class (stl/css :swatch-solid-side)
|
||||
:style {:background (uc/color->background (assoc background :opacity 1))}}]
|
||||
[:span {:class (stl/css :swatch-opacity-side)
|
||||
:style {:background (uc/color->background background)}}]])]))
|
||||
:else
|
||||
[:span {:class (stl/css :swatch-opacity)}
|
||||
[:span {:class (stl/css :swatch-solid-side)
|
||||
:style {:background (uc/color->background (assoc background :opacity 1))}}]
|
||||
[:span {:class (stl/css :swatch-opacity-side)
|
||||
:style {:background (uc/color->background background)}}]])]]))
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
--checkerboard-background: repeating-conic-gradient(lightgray 0% 25%, white 0% 50%);
|
||||
--checkerboard-size: 0.5rem 0.5rem;
|
||||
|
||||
border: 1px solid var(--border-color);
|
||||
border: $b-1 solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
@@ -263,6 +263,7 @@
|
||||
(def ^:icon tokens (icon-xref :tokens))
|
||||
(def ^:icon to-corner (icon-xref :to-corner))
|
||||
(def ^:icon to-curve (icon-xref :to-curve))
|
||||
(def ^:icon tokens (icon-xref :tokens))
|
||||
(def ^:icon tree (icon-xref :tree))
|
||||
(def ^:icon unlock (icon-xref :unlock))
|
||||
(def ^:icon user (icon-xref :user))
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.color :as cc]
|
||||
[app.common.types.fills :as types.fills]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.event :as-alias ev]
|
||||
[app.main.data.modal :as modal]
|
||||
@@ -26,12 +27,14 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||
[app.main.ui.components.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.components.radio-buttons :refer [radio-buttons radio-button]]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]]
|
||||
[app.main.ui.workspace.colorpicker.color-tokens :refer [token-section*]]
|
||||
[app.main.ui.workspace.colorpicker.gradients :refer [gradients*]]
|
||||
[app.main.ui.workspace.colorpicker.harmony :refer [harmony-selector]]
|
||||
[app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector]]
|
||||
@@ -90,12 +93,20 @@
|
||||
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
|
||||
|
||||
(mf/defc colorpicker
|
||||
{::mf/props :obj}
|
||||
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}]
|
||||
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change]}]
|
||||
(let [state (mf/deref refs/colorpicker)
|
||||
node-ref (mf/use-ref)
|
||||
|
||||
should-update? (mf/use-var true)
|
||||
token-color (contains? cfg/flags :token-color)
|
||||
color-style* (mf/use-state :direct-color)
|
||||
color-style (deref color-style*)
|
||||
toggle-token-color
|
||||
(mf/use-fn
|
||||
(mf/deps color-style)
|
||||
(fn []
|
||||
(let [new-style (if (= :direct-color color-style) :token-color :direct-color)]
|
||||
(reset! color-style* new-style))))
|
||||
|
||||
;; TODO: I think we need to put all this picking state under
|
||||
;; the same object for avoid creating adhoc refs for each
|
||||
@@ -382,8 +393,10 @@
|
||||
:ref node-ref
|
||||
:style {:touch-action "none"}}
|
||||
[:div {:class (stl/css :top-actions)}
|
||||
|
||||
[:div {:class (stl/css :top-actions-right)}
|
||||
(when (= :gradient selected-mode)
|
||||
(when (and (= color-style :direct-color)
|
||||
(= :gradient selected-mode))
|
||||
[:div {:class (stl/css :opacity-input-wrapper)}
|
||||
[:span {:class (stl/css :icon-text)} "%"]
|
||||
[:> numeric-input*
|
||||
@@ -394,118 +407,143 @@
|
||||
:min 0
|
||||
:max 100}]])
|
||||
|
||||
(when (or (not disable-gradient) (not disable-image))
|
||||
(when (and (= color-style :direct-color)
|
||||
(or (not disable-gradient) (not disable-image)))
|
||||
[:div {:class (stl/css :select)}
|
||||
[:& select
|
||||
{:default-value selected-mode
|
||||
:options options
|
||||
:on-change handle-change-mode}]])]
|
||||
:on-change handle-change-mode}]])
|
||||
|
||||
(when (not= selected-mode :image)
|
||||
(when (and (= origin :sidebar) token-color)
|
||||
[:& radio-buttons {:selected color-style
|
||||
:on-change toggle-token-color
|
||||
:name "color-style"}
|
||||
[:& radio-button {:icon deprecated-icon/swatches
|
||||
:value :direct-color
|
||||
:title (tr "labels.color")
|
||||
:id "opt-color"}]
|
||||
[:& radio-button {:icon deprecated-icon/tokens
|
||||
:value :token-color
|
||||
:title (tr "workspace.colorpicker.color-tokens")
|
||||
:id "opt-token-color"}]])]
|
||||
|
||||
(when (and (not= selected-mode :image)
|
||||
(= color-style :direct-color))
|
||||
[:button {:class (stl/css-case :picker-btn true
|
||||
:selected picking-color?)
|
||||
:on-click handle-click-picker}
|
||||
deprecated-icon/picker])]
|
||||
deprecated-icon/picker])
|
||||
|
||||
(when (= selected-mode :gradient)
|
||||
[:> gradients*
|
||||
{:type (:type state)
|
||||
:stops (if cap-stops? (vec (take types.fills/MAX-GRADIENT-STOPS (:stops state))) (:stops state))
|
||||
:editing-stop (:editing-stop state)
|
||||
:on-stop-edit-start handle-stop-edit-start
|
||||
:on-stop-edit-finish handle-stop-edit-finish
|
||||
:on-select-stop handle-change-gradient-selected-stop
|
||||
:on-change-type handle-change-gradient-type
|
||||
:on-change-stop handle-gradient-change-stop
|
||||
:on-add-stop-auto handle-gradient-add-stop-auto
|
||||
:on-add-stop-preview handle-gradient-add-stop-preview
|
||||
:on-remove-stop handle-gradient-remove-stop
|
||||
:on-rotate-stops handle-rotate-stops
|
||||
:on-reverse-stops handle-reverse-stops
|
||||
:on-reorder-stops handle-reorder-stops}])
|
||||
|
||||
(if (= selected-mode :image)
|
||||
(let [uri (cfg/resolve-file-media (:image current-color))
|
||||
keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio)]
|
||||
[:div {:class (stl/css :select-image)}
|
||||
[:div {:class (stl/css :content)}
|
||||
(when (:image current-color)
|
||||
[:img {:src uri}])]
|
||||
|
||||
(when (some? (:image current-color))
|
||||
[:div {:class (stl/css :checkbox-option)}
|
||||
[:label {:for "keep-aspect-ratio"
|
||||
:class (stl/css-case :global/checked keep-aspect-ratio?)}
|
||||
[:span {:class (stl/css-case :global/checked keep-aspect-ratio?)}
|
||||
(when keep-aspect-ratio?
|
||||
deprecated-icon/status-tick)]
|
||||
(tr "media.keep-aspect-ratio")
|
||||
[:input {:type "checkbox"
|
||||
:id "keep-aspect-ratio"
|
||||
:checked keep-aspect-ratio?
|
||||
:on-change handle-change-keep-aspect-ratio}]]])
|
||||
[:button
|
||||
{:class (stl/css :choose-image)
|
||||
:title (tr "media.choose-image")
|
||||
:aria-label (tr "media.choose-image")
|
||||
:on-click on-fill-image-click}
|
||||
(tr "media.choose-image")
|
||||
[:& file-uploader
|
||||
{:input-id "fill-image-upload"
|
||||
:accept "image/jpeg,image/png"
|
||||
:multi false
|
||||
:ref fill-image-ref
|
||||
:on-selected on-fill-image-selected}]]])
|
||||
(when (= color-style :token-color)
|
||||
[:div {:class (stl/css :token-color-title)}
|
||||
(tr "workspace.colorpicker.color-tokens")])]
|
||||
|
||||
(if (= color-style :direct-color)
|
||||
[:*
|
||||
[:div {:class (stl/css :colorpicker-tabs)}
|
||||
[:> tab-switcher* {:tabs tabs
|
||||
:selected active-color-tab
|
||||
:on-change on-change-tab}
|
||||
(if picking-color?
|
||||
[:div {:class (stl/css :picker-detail-wrapper)}
|
||||
[:div {:class (stl/css :center-circle)}]
|
||||
[:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]]
|
||||
(when (= selected-mode :gradient)
|
||||
[:> gradients*
|
||||
{:type (:type state)
|
||||
:stops (if cap-stops? (vec (take types.fills/MAX-GRADIENT-STOPS (:stops state))) (:stops state))
|
||||
:editing-stop (:editing-stop state)
|
||||
:on-stop-edit-start handle-stop-edit-start
|
||||
:on-stop-edit-finish handle-stop-edit-finish
|
||||
:on-select-stop handle-change-gradient-selected-stop
|
||||
:on-change-type handle-change-gradient-type
|
||||
:on-change-stop handle-gradient-change-stop
|
||||
:on-add-stop-auto handle-gradient-add-stop-auto
|
||||
:on-add-stop-preview handle-gradient-add-stop-preview
|
||||
:on-remove-stop handle-gradient-remove-stop
|
||||
:on-rotate-stops handle-rotate-stops
|
||||
:on-reverse-stops handle-reverse-stops
|
||||
:on-reorder-stops handle-reorder-stops}])
|
||||
|
||||
(if (= selected-mode :image)
|
||||
(let [uri (cfg/resolve-file-media (:image current-color))
|
||||
keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio)]
|
||||
[:div {:class (stl/css :select-image)}
|
||||
[:div {:class (stl/css :content)}
|
||||
(when (:image current-color)
|
||||
[:img {:src uri}])]
|
||||
|
||||
(when (some? (:image current-color))
|
||||
[:div {:class (stl/css :checkbox-option)}
|
||||
[:label {:for "keep-aspect-ratio"
|
||||
:class (stl/css-case :global/checked keep-aspect-ratio?)}
|
||||
[:span {:class (stl/css-case :global/checked keep-aspect-ratio?)}
|
||||
(when keep-aspect-ratio?
|
||||
deprecated-icon/status-tick)]
|
||||
(tr "media.keep-aspect-ratio")
|
||||
[:input {:type "checkbox"
|
||||
:id "keep-aspect-ratio"
|
||||
:checked keep-aspect-ratio?
|
||||
:on-change handle-change-keep-aspect-ratio}]]])
|
||||
[:button
|
||||
{:class (stl/css :choose-image)
|
||||
:title (tr "media.choose-image")
|
||||
:aria-label (tr "media.choose-image")
|
||||
:on-click on-fill-image-click}
|
||||
(tr "media.choose-image")
|
||||
[:& file-uploader
|
||||
{:input-id "fill-image-upload"
|
||||
:accept "image/jpeg,image/png"
|
||||
:multi false
|
||||
:ref fill-image-ref
|
||||
:on-selected on-fill-image-selected}]]])
|
||||
|
||||
[:*
|
||||
[:div {:class (stl/css :colorpicker-tabs)}
|
||||
[:> tab-switcher* {:tabs tabs
|
||||
:selected active-color-tab
|
||||
:on-change on-change-tab}
|
||||
(if picking-color?
|
||||
[:div {:class (stl/css :picker-detail-wrapper)}
|
||||
[:div {:class (stl/css :center-circle)}]
|
||||
[:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]]
|
||||
|
||||
|
||||
(case active-color-tab
|
||||
"ramp"
|
||||
[:> ramp-selector*
|
||||
{:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:on-change handle-change-color
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
(case active-color-tab
|
||||
"ramp"
|
||||
[:> ramp-selector*
|
||||
{:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:on-change handle-change-color
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]
|
||||
|
||||
"harmony"
|
||||
[:& harmony-selector
|
||||
{:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:on-change handle-change-color
|
||||
:on-start-drag on-start-drag}]
|
||||
"harmony"
|
||||
[:& harmony-selector
|
||||
{:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:on-change handle-change-color
|
||||
:on-start-drag on-start-drag}]
|
||||
|
||||
"hsva"
|
||||
[:& hsva-selector
|
||||
{:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:on-change handle-change-color
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]))]]
|
||||
"hsva"
|
||||
[:& hsva-selector
|
||||
{:color current-color
|
||||
:disable-opacity disable-opacity
|
||||
:on-change handle-change-color
|
||||
:on-start-drag on-start-drag
|
||||
:on-finish-drag on-finish-drag}]))]]
|
||||
|
||||
[:& color-inputs
|
||||
{:type type
|
||||
:disable-opacity disable-opacity
|
||||
:color current-color
|
||||
:on-change handle-change-color}]
|
||||
[:& color-inputs
|
||||
{:type type
|
||||
:disable-opacity disable-opacity
|
||||
:color current-color
|
||||
:on-change handle-change-color}]
|
||||
|
||||
[:& libraries
|
||||
{:state state
|
||||
:current-color current-color
|
||||
:disable-gradient disable-gradient
|
||||
:disable-opacity disable-opacity
|
||||
:disable-image disable-image
|
||||
:on-select-color on-select-library-color
|
||||
:on-add-library-color on-add-library-color}]])]
|
||||
[:& libraries
|
||||
{:state state
|
||||
:current-color current-color
|
||||
:disable-gradient disable-gradient
|
||||
:disable-opacity disable-opacity
|
||||
:disable-image disable-image
|
||||
:on-select-color on-select-library-color
|
||||
:on-add-library-color on-add-library-color}]])]
|
||||
|
||||
[:> token-section* {:combined-tokens combined-tokens
|
||||
:on-token-change on-token-change
|
||||
:color-origin color-origin}])]
|
||||
(when (fn? on-accept)
|
||||
[:div {:class (stl/css :actions)}
|
||||
[:button {:class (stl/css-case
|
||||
@@ -561,6 +599,115 @@
|
||||
:top top-offset
|
||||
:maxHeight max-height-top}))))
|
||||
|
||||
(defn- group-sets
|
||||
"Groups sets by their parent path (everything before the last '/') if present.
|
||||
The set name is always the last part of the path.
|
||||
|
||||
Input:
|
||||
[{:set \"brand/subgroup/one\" :tokens [{:name \"background\"}]}
|
||||
{:set \"brand/subgroup/two\" :tokens [{:name \"foreground\"}]}
|
||||
{:set \"primitives\" :tokens [{:name \"blue-100\"}]}]
|
||||
|
||||
Output:
|
||||
[{:group \"brand/subgroup\"
|
||||
:sets [\"one\" \"two\"]
|
||||
:tokens [\"background\" \"foreground\"]}
|
||||
{:group nil
|
||||
:sets [\"primitives\"]
|
||||
:tokens [\"blue-100\"]}]"
|
||||
|
||||
[sets]
|
||||
(->> sets
|
||||
(group-by (fn [{:keys [set]}]
|
||||
(when (str/includes? set "/")
|
||||
(str/join "/" (butlast (str/split set #"/"))))))
|
||||
(map (fn [[group grouped-sets]]
|
||||
(if group
|
||||
{:group group
|
||||
:sets (map #(last (str/split (:set %) #"/")) grouped-sets)
|
||||
:tokens (->> grouped-sets
|
||||
(mapcat :tokens)
|
||||
(map :name)
|
||||
distinct)}
|
||||
(map (fn [{:keys [set tokens]}]
|
||||
{:group nil
|
||||
:sets [set]
|
||||
:tokens (map :name tokens)})
|
||||
grouped-sets))))
|
||||
flatten))
|
||||
|
||||
(defn- combine-groups-with-resolved
|
||||
"Replaces token names in grouped sets with their full resolved token objects.
|
||||
|
||||
Input:
|
||||
- groups: [{:group \"brand\"
|
||||
:sets [\"light\" \"dark\"]
|
||||
:tokens [\"background\" \"foreground\"]} ...]
|
||||
- resolved-tokens: [{:name \"background\" :type \"color\" :value \"{red-100}\" ...} ...]
|
||||
|
||||
Output:
|
||||
[{:group \"brand\"
|
||||
:sets [\"light\" \"dark\"]
|
||||
:tokens [{:name \"background\" :type \"color\" :value \"{red-100}\" ...}
|
||||
{:name \"foreground\" :type \"color\" :value \"{green-100}\" ...}]}]"
|
||||
|
||||
[groups resolved-tokens]
|
||||
(let [token-map (into {} (map (juxt :name identity) resolved-tokens))]
|
||||
(map (fn [{:keys [group sets tokens]}]
|
||||
{:group group
|
||||
:sets sets
|
||||
:tokens (->> tokens
|
||||
(map #(get token-map %))
|
||||
(remove nil?)
|
||||
vec)})
|
||||
groups)))
|
||||
|
||||
(defn- filter-non-empty-sets
|
||||
"Removes sets that have no tokens.
|
||||
|
||||
Input:
|
||||
[{:set \"brand/light\" :tokens []}
|
||||
{:set \"brand/dark\" :tokens [{:name \"background\"}]}]
|
||||
|
||||
Output:
|
||||
[{:set \"brand/dark\" :tokens [{:name \"background\"}]}]"
|
||||
[sets]
|
||||
(filter (fn [{:keys [tokens]}]
|
||||
(seq tokens))
|
||||
sets))
|
||||
|
||||
(defn- add-tokens-to-sets
|
||||
"Extracts set name and its tokens from raw set objects.
|
||||
|
||||
Input:
|
||||
A vector of set objects (raw domain type), each compatible with:
|
||||
{:id ... :name \"brand/light\" :tokens {...}}
|
||||
|
||||
Output:
|
||||
A vector of simplified maps:
|
||||
[{:set \"brand/light\" :tokens [{:name \"background\" ...} ...]}]"
|
||||
[sets]
|
||||
(map (fn [s]
|
||||
{:set (ctob/get-name s)
|
||||
:tokens (ctob/get-tokens s)})
|
||||
sets))
|
||||
|
||||
(defn- filter-active-sets
|
||||
"Filters sets to only include those whose :set value is in active-set-names.
|
||||
|
||||
Input:
|
||||
- sets: [{:set \"brand/light\" :tokens [...]},
|
||||
{:set \"brand/dark\" :tokens [...]},
|
||||
{:set \"primitivos\" :tokens [...]},
|
||||
...]
|
||||
- active-set-names: #{\"brand/light\" \"primitivos\"}
|
||||
|
||||
Output:
|
||||
[{:set \"brand/light\" :tokens [...]}
|
||||
{:set \"primitivos\" :tokens [...]}]"
|
||||
[sets active-set-names]
|
||||
(filter #(contains? active-set-names (:set %)) sets))
|
||||
|
||||
(mf/defc colorpicker-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :colorpicker
|
||||
@@ -569,7 +716,11 @@
|
||||
disable-gradient
|
||||
disable-opacity
|
||||
disable-image
|
||||
active-tokens
|
||||
on-change
|
||||
origin
|
||||
color-origin
|
||||
on-token-change
|
||||
on-close
|
||||
on-accept]}]
|
||||
(let [vport (mf/deref viewport)
|
||||
@@ -587,7 +738,27 @@
|
||||
(reset! last-change new-data)
|
||||
(if (fn? on-change)
|
||||
(on-change new-data)
|
||||
(st/emit! (dc/update-colorpicker new-data))))))]
|
||||
(st/emit! (dc/update-colorpicker new-data))))))
|
||||
|
||||
tokens-lib
|
||||
(mf/deref refs/tokens-lib)
|
||||
|
||||
active-sets-names
|
||||
(mf/with-memo [tokens-lib]
|
||||
(some-> tokens-lib
|
||||
(ctob/get-active-themes-set-names)))
|
||||
|
||||
color-tokens (:color active-tokens)
|
||||
|
||||
grouped-tokens-by-set
|
||||
(mf/with-memo [tokens-lib active-sets-names color-tokens]
|
||||
(some-> tokens-lib
|
||||
(ctob/get-sets)
|
||||
(add-tokens-to-sets)
|
||||
(filter-active-sets active-sets-names)
|
||||
(filter-non-empty-sets)
|
||||
(group-sets)
|
||||
(combine-groups-with-resolved color-tokens)))]
|
||||
|
||||
(mf/with-effect []
|
||||
(st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts)))
|
||||
@@ -601,8 +772,12 @@
|
||||
:style style}
|
||||
|
||||
[:& colorpicker {:data data
|
||||
:combined-tokens grouped-tokens-by-set
|
||||
:disable-gradient disable-gradient
|
||||
:disable-opacity disable-opacity
|
||||
:disable-image disable-image
|
||||
:on-token-change on-token-change
|
||||
:on-change on-change'
|
||||
:origin origin
|
||||
:color-origin color-origin
|
||||
:on-accept on-accept}]]))
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "../ds/typography.scss" as t;
|
||||
@use "../ds/spacing.scss";
|
||||
@use "../ds/_borders.scss" as *;
|
||||
@use "../ds/_sizes.scss" as *;
|
||||
@import "refactor/common-refactor.scss";
|
||||
@import "../ds/_sizes.scss";
|
||||
|
||||
.colorpicker-tooltip {
|
||||
@extend .modal-background;
|
||||
@@ -23,22 +26,17 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.colorpicker-tabs {
|
||||
padding: 0 var(--sp-m);
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
height: $s-40;
|
||||
padding: 0 var(--sp-m);
|
||||
}
|
||||
|
||||
.top-actions-right {
|
||||
display: flex;
|
||||
gap: $s-8;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.opacity-input-wrapper {
|
||||
@@ -52,12 +50,12 @@
|
||||
@include flexCenter;
|
||||
border-radius: $br-8;
|
||||
background-color: transparent;
|
||||
border: $s-1 solid transparent;
|
||||
height: $s-20;
|
||||
width: $s-20;
|
||||
border: $b-1 solid transparent;
|
||||
height: var(--sp-xl);
|
||||
width: var(--sp-xl);
|
||||
border-radius: $br-4;
|
||||
padding: 0;
|
||||
margin-top: $s-4;
|
||||
margin-top: var(--sp-xs);
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
stroke: var(--button-tertiary-foreground-color-rest);
|
||||
@@ -76,7 +74,7 @@
|
||||
}
|
||||
&:active {
|
||||
outline: none;
|
||||
border: $s-1 solid transparent;
|
||||
border: $b-1 solid transparent;
|
||||
svg {
|
||||
stroke: var(--button-tertiary-foreground-color-active);
|
||||
}
|
||||
@@ -91,17 +89,17 @@
|
||||
.gradient-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $s-8;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.gradient-btn {
|
||||
@extend .button-tertiary;
|
||||
height: $s-20;
|
||||
width: $s-20;
|
||||
height: var(--sp-xl);
|
||||
width: var(--sp-xl);
|
||||
border-radius: $br-4;
|
||||
border: $s-2 solid transparent;
|
||||
border: $b-2 solid transparent;
|
||||
&:hover {
|
||||
border: $s-2 solid var(--colorpicker-details-color-selected);
|
||||
border: $b-2 solid var(--colorpicker-details-color-selected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +107,7 @@
|
||||
background: linear-gradient(180deg, var(--color-foreground-secondary), transparent);
|
||||
&.selected {
|
||||
background: linear-gradient(to bottom, rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%);
|
||||
border: $s-2 solid var(--colorpicker-details-color-selected);
|
||||
border: $b-2 solid var(--colorpicker-details-color-selected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,38 +115,38 @@
|
||||
background: radial-gradient(transparent, var(--color-foreground-secondary));
|
||||
&.selected {
|
||||
background: radial-gradient(rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%);
|
||||
border: $s-2 solid var(--colorpicker-details-color-selected);
|
||||
border: $b-2 solid var(--colorpicker-details-color-selected);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: $s-4;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.accept-color {
|
||||
@include uppercaseTitleTipography;
|
||||
@extend .button-primary;
|
||||
width: 100%;
|
||||
height: $s-32;
|
||||
margin-top: $s-8;
|
||||
height: var(--sp-xxxl);
|
||||
margin-top: var(--sp-s);
|
||||
}
|
||||
|
||||
.picker-detail-wrapper {
|
||||
@include flexCenter;
|
||||
position: relative;
|
||||
margin: $s-12 0 $s-8 0;
|
||||
margin: var(--sp-m) 0 var(--sp-s) 0;
|
||||
}
|
||||
|
||||
.center-circle {
|
||||
width: $s-24;
|
||||
height: $s-24;
|
||||
border: $s-2 solid var(--colorpicker-details-color);
|
||||
width: var(--sp-xxl);
|
||||
height: var(--sp-xxl);
|
||||
border: $b-2 solid var(--colorpicker-details-color);
|
||||
border-radius: $br-circle;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(calc(-1 * $s-12), calc(-1 * $s-12));
|
||||
transform: translate(calc(-1 * var(--sp-m)), calc(-1 * var(--sp-m)));
|
||||
}
|
||||
|
||||
.picker-detail {
|
||||
@@ -161,7 +159,7 @@
|
||||
}
|
||||
|
||||
.select-image {
|
||||
margin-top: $s-4;
|
||||
margin-top: var(--sp-xs);
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -172,8 +170,8 @@
|
||||
background-position: center;
|
||||
background-size: auto $s-140;
|
||||
height: $s-140;
|
||||
margin-bottom: $s-6;
|
||||
margin-right: $s-1;
|
||||
margin-bottom: $sz-6;
|
||||
margin-right: $sz-1;
|
||||
img {
|
||||
height: fit-content;
|
||||
width: fit-content;
|
||||
@@ -187,11 +185,19 @@
|
||||
@extend .button-secondary;
|
||||
@include uppercaseTitleTipography;
|
||||
width: 100%;
|
||||
margin-top: $s-12;
|
||||
height: $s-32;
|
||||
margin-top: var(--sp-m);
|
||||
height: var(--sp-xxxl);
|
||||
}
|
||||
|
||||
.checkbox-option {
|
||||
@extend .input-checkbox;
|
||||
margin: $s-16 0 0 0;
|
||||
margin: var(--sp-l) 0 0 0;
|
||||
}
|
||||
|
||||
.token-color-title {
|
||||
@include t.use-typography("title-small");
|
||||
color: var(--color-foreground-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--sp-xxxl);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
.color-values {
|
||||
@include flexColumn;
|
||||
margin-top: $s-8;
|
||||
padding: 0 var(--sp-m);
|
||||
|
||||
&.disable-opacity {
|
||||
grid-template-columns: 3.5rem repeat(3, 1fr);
|
||||
|
||||
295
frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs
Normal file
295
frontend/src/app/main/ui/workspace/colorpicker/color_tokens.cljs
Normal file
@@ -0,0 +1,295 @@
|
||||
;; 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.colorpicker.color-tokens
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.constants :refer [max-input-length]]
|
||||
[app.main.data.event :as-alias ev]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.title-bar :refer [title-bar*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.timers :as tm]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc token-empty-state*
|
||||
{::mf/private true}
|
||||
[]
|
||||
[:div {:class (stl/css :color-token-empty-state)}
|
||||
(tr "color-token.empty-state")])
|
||||
|
||||
(mf/defc list-item*
|
||||
{::mf/private true}
|
||||
[{:keys [token on-token-pill-click selected] :rest props}]
|
||||
(let [on-click
|
||||
(mf/use-fn
|
||||
(mf/deps token on-token-pill-click)
|
||||
(fn [event]
|
||||
(on-token-pill-click event token)))
|
||||
id-tooltip (mf/use-id)
|
||||
resolved (:resolved-value token)
|
||||
color-value (dwta/value->color resolved)]
|
||||
[:> tooltip* {:id id-tooltip
|
||||
:style {:width "100%"}
|
||||
:content (:name token)}
|
||||
[:button {:class (stl/css-case :color-token-item true
|
||||
:color-token-selected selected)
|
||||
:aria-labelledby id-tooltip
|
||||
:on-click on-click}
|
||||
[:> swatch* {:background color-value
|
||||
:tooltip-content (tr "workspace.tokens.resolved-value" resolved)
|
||||
:size "small"}]
|
||||
[:div {:class (stl/css :token-name)}
|
||||
(:name token)]
|
||||
(when selected
|
||||
[:> i/icon* {:icon-id i/tick
|
||||
:size "s"
|
||||
:class (stl/css :token-selected-icon)}])]]))
|
||||
|
||||
(mf/defc grid-item*
|
||||
{::mf/private true}
|
||||
[{:keys [token on-token-pill-click selected] :rest props}]
|
||||
(let [on-click
|
||||
(mf/use-fn
|
||||
(mf/deps token on-token-pill-click)
|
||||
(fn [event]
|
||||
(on-token-pill-click event token)))
|
||||
resolved (:resolved-value token)
|
||||
token-name (:name token)
|
||||
color-value (dwta/value->color resolved)]
|
||||
[:div {:class (stl/css-case :color-token-item-grid true
|
||||
:color-token-selected-grid selected)}
|
||||
[:> swatch* {:background color-value
|
||||
:tooltip-content
|
||||
(mf/html
|
||||
[:*
|
||||
[:div (dm/str (tr "workspace.tokens.token-name") ": " token-name)]
|
||||
[:div (tr "workspace.tokens.resolved-value" resolved)]])
|
||||
:on-click on-click
|
||||
:size "medium"}]]))
|
||||
|
||||
(defn group->set-name
|
||||
"Given a group structure, returns a representative set name.
|
||||
|
||||
Input:
|
||||
{:group \"brand\"
|
||||
:sets [\"light\" \"dark\"]
|
||||
:tokens [...]}
|
||||
|
||||
Output:
|
||||
- If :group exists → \"brand/light\" (first set in :sets)
|
||||
- If :group is nil → the first (and only) value of :sets"
|
||||
[{:keys [group sets]}]
|
||||
(if group
|
||||
(str group "/" (first sets))
|
||||
(first sets)))
|
||||
|
||||
(mf/defc set-section*
|
||||
{::mf/private true}
|
||||
[{:keys [collapsed toggle-sets-open set name color-origin on-token-change] :rest props}]
|
||||
|
||||
(let [list-style* (mf/use-state :list)
|
||||
list-style (deref list-style*)
|
||||
toggle-list-style
|
||||
(mf/use-fn
|
||||
(mf/deps list-style)
|
||||
(fn []
|
||||
(let [new-style (if (= :list list-style) :grid :list)]
|
||||
(reset! list-style* new-style))))
|
||||
|
||||
toggle-set
|
||||
(mf/use-fn
|
||||
(mf/deps name toggle-sets-open)
|
||||
(fn []
|
||||
(toggle-sets-open name)))
|
||||
|
||||
objects (mf/deref refs/workspace-page-objects)
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
|
||||
selected-shapes
|
||||
(mf/with-memo [selected objects]
|
||||
(into [] (keep (d/getf objects)) selected))
|
||||
first-shape (first selected-shapes)
|
||||
applied-tokens (:applied-tokens first-shape)
|
||||
has-color-tokens? (get applied-tokens :fill)
|
||||
has-stroke-tokens? (get applied-tokens :stroke-color)
|
||||
|
||||
on-token-pill-click
|
||||
(mf/use-fn
|
||||
(mf/deps selected-shapes selected color-origin)
|
||||
(fn [event token]
|
||||
(dom/stop-propagation event)
|
||||
(when (seq selected-shapes)
|
||||
(if (= :color-selection color-origin)
|
||||
(on-token-change event token)
|
||||
(let [attributes (if (= color-origin :stroke) #{:stroke-color} #{:fill})
|
||||
shape-ids (into #{} (map :id selected-shapes))]
|
||||
(if (or
|
||||
(= (:name token) has-stroke-tokens?)
|
||||
(= (:name token) has-color-tokens?))
|
||||
(st/emit! (dwta/unapply-token {:attributes attributes
|
||||
:token token
|
||||
:shape-ids shape-ids}))
|
||||
(st/emit! (dwta/apply-token {:shape-ids shape-ids
|
||||
:attributes attributes
|
||||
:token token
|
||||
:on-update-shape dwta/update-fill-stroke}))))))))
|
||||
|
||||
create-token-on-set
|
||||
(mf/use-fn
|
||||
(mf/deps set)
|
||||
(fn [_]
|
||||
(let [first-set-name (group->set-name set)
|
||||
set-item-id (dm/str "token-set-item-" first-set-name)
|
||||
set-element (dom/get-element set-item-id)]
|
||||
(when set-element
|
||||
(dom/click set-element)
|
||||
(tm/schedule-on-idle
|
||||
(let [button-element (dom/get-element "add-token-button-Color")]
|
||||
#(dom/click button-element)))))))]
|
||||
|
||||
[:div {:class (stl/css :color-token-set)}
|
||||
[:> title-bar* {:collapsable true
|
||||
:collapsed collapsed
|
||||
:all-clickable true
|
||||
:on-collapsed toggle-set
|
||||
:class (stl/css :set-title-bar)
|
||||
:title name}
|
||||
|
||||
(when (not collapsed)
|
||||
[:div {:class (stl/css :set-actions)}
|
||||
[:> icon-button* {:on-click toggle-list-style
|
||||
:variant "action"
|
||||
:aria-label (if (= :list list-style)
|
||||
(tr "workspace.assets.grid-view")
|
||||
(tr "workspace.assets.list-view"))
|
||||
:icon (if (= :list list-style)
|
||||
i/flex-grid
|
||||
i/view-as-list)}]
|
||||
[:> icon-button* {:on-click create-token-on-set
|
||||
:variant "action"
|
||||
:aria-label (tr "workspace.tokens.add-token" "color")
|
||||
:icon i/add}]])]
|
||||
|
||||
(when (not collapsed)
|
||||
[:div {:class (stl/css-case :color-token-list true
|
||||
:list-view (= list-style :list)
|
||||
:grid-view (= list-style :grid))}
|
||||
|
||||
(for [token (:tokens set)]
|
||||
(let [selected? (if (= color-origin :fill)
|
||||
(= has-color-tokens? (:name token))
|
||||
(= has-stroke-tokens? (:name token)))]
|
||||
(if (= :grid list-style)
|
||||
[:> grid-item* {:key (str "token-grid-" (:id token))
|
||||
:on-token-pill-click on-token-pill-click
|
||||
:selected selected?
|
||||
:token token}]
|
||||
[:> list-item* {:key (str "token-list-" (:id token))
|
||||
:on-token-pill-click on-token-pill-click
|
||||
:selected selected?
|
||||
:token token}])))])]))
|
||||
(defn- label-group-or-set [{:keys [group sets]}]
|
||||
(if group
|
||||
(str group " (" (str/join ", " sets) ")")
|
||||
(first sets)))
|
||||
|
||||
(defn- filter-combined-tokens
|
||||
"Filters the combined-tokens structure by token name.
|
||||
Removes sets or groups if they end up with no tokens.
|
||||
|
||||
Input:
|
||||
[{:group \"brand\", :sets [\"light\" \"dark\"], :tokens [{:name \"background\"} {:name \"foreground\"}]}
|
||||
{:group nil, :sets [\"primitivos\"], :tokens [{:name \"blue-100\"} {:name \"red-100\"}]}]
|
||||
|
||||
(filter-combined-tokens ... \"blue\")
|
||||
Output:
|
||||
[{:group nil, :sets [\"primitivos\"], :tokens [{:name \"blue-100\"}]}]
|
||||
=> keeps only tokens matching \"blue\", and removes sets/groups if no tokens match."
|
||||
|
||||
[combined-tokens term]
|
||||
(let [term (str/lower (str/trim term))]
|
||||
(if (str/blank? term)
|
||||
combined-tokens
|
||||
(->> combined-tokens
|
||||
(map (fn [{:keys [tokens] :as entry}]
|
||||
(let [filtered (filter #(str/includes?
|
||||
(str/lower (:name %))
|
||||
term)
|
||||
tokens)]
|
||||
(when (seq filtered)
|
||||
(assoc entry :tokens filtered)))))
|
||||
(remove nil?)))))
|
||||
|
||||
(defn- sort-combined-tokens
|
||||
"Sorts tokens alphabetically by :name inside each group/set.
|
||||
Input:
|
||||
[{:group \"brand\", :sets [\"light\" \"dark\"], :tokens [{:name \"foreground\"} {:name \"background\"}]}]
|
||||
|
||||
Output:
|
||||
[{:group \"brand\", :sets [\"light\" \"dark\"], :tokens [{:name \"background\"} {:name \"foreground\"}]}]"
|
||||
[combined-tokens]
|
||||
(map (fn [entry]
|
||||
(update entry :tokens #(sort-by :name %)))
|
||||
combined-tokens))
|
||||
|
||||
(mf/defc token-section*
|
||||
{}
|
||||
[{:keys [combined-tokens color-origin on-token-change] :rest props}]
|
||||
(let [sets (set (mapv label-group-or-set combined-tokens))
|
||||
filter-term* (mf/use-state "")
|
||||
filter-term (deref filter-term*)
|
||||
open-sets* (mf/use-state sets)
|
||||
open-sets (deref open-sets*)
|
||||
toggle-sets-open
|
||||
(mf/use-fn
|
||||
(mf/deps open-sets)
|
||||
(fn [name]
|
||||
(if (contains? open-sets name)
|
||||
(swap! open-sets* disj name)
|
||||
(swap! open-sets* conj name))))
|
||||
|
||||
on-filter-tokens
|
||||
(mf/use-fn
|
||||
(mf/deps filter-term)
|
||||
(fn [event]
|
||||
(let [value (-> event (dom/get-target)
|
||||
(dom/get-value))]
|
||||
(reset! filter-term* value))))
|
||||
filtered-combined (filter-combined-tokens combined-tokens filter-term)
|
||||
sorted-tokens (sort-combined-tokens filtered-combined)]
|
||||
(if combined-tokens
|
||||
[:div {:class (stl/css :color-tokens-section)}
|
||||
[:> input* {:placeholder "Search by token name"
|
||||
:icon i/search
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:class (stl/css :search-input)
|
||||
:default-value filter-term
|
||||
:on-change on-filter-tokens}]
|
||||
(for [combined-sets sorted-tokens]
|
||||
(let [name (label-group-or-set combined-sets)]
|
||||
[:> set-section*
|
||||
{:collapsed (not (contains? open-sets name))
|
||||
:key (str "set-" name)
|
||||
:toggle-sets-open toggle-sets-open
|
||||
:color-origin color-origin
|
||||
:on-token-change on-token-change
|
||||
:name name
|
||||
:set combined-sets}]))]
|
||||
[:> token-empty-state*])))
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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 t;
|
||||
@use "../../ds/_borders.scss" as *;
|
||||
@use "../../ds/_sizes.scss" as *;
|
||||
|
||||
.color-token-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.color-token-item {
|
||||
--color-token-background: var(--color-background-primary);
|
||||
background-color: var(--color-token-background);
|
||||
color: var(--color-foreground-primary);
|
||||
text-align: left;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
width: 100%;
|
||||
border-radius: $br-8;
|
||||
padding: var(--sp-xs);
|
||||
height: var(--sp-xxl);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
--color-token-background: var(--color-background-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.color-token-empty-state {
|
||||
@include t.use-typography("body-small");
|
||||
padding: var(--sp-s) var(--sp-xxl);
|
||||
text-align: center;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.color-token-selected {
|
||||
background-color: var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
.color-token-selected-grid {
|
||||
border: $b-1 solid var(--color-accent-primary);
|
||||
border-radius: $br-4;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.token-selected-icon {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.token-name {
|
||||
@include t.use-typography("body-small");
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.color-tokens-section {
|
||||
max-height: $sz-430;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.set-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.grid-view {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--sp-xxl), 1fr));
|
||||
justify-items: start;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.list-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.set-title-bar {
|
||||
@include t.use-typography("title-small");
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: $sz-1;
|
||||
}
|
||||
@@ -158,6 +158,7 @@
|
||||
:disable-picker true
|
||||
:color stop
|
||||
:index index
|
||||
:origin :gradient
|
||||
:on-change handle-change-stop-color
|
||||
:on-remove handle-remove-stop
|
||||
:on-focus handle-focus-stop-color
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
.libraries {
|
||||
margin-top: $s-8;
|
||||
width: 100%;
|
||||
padding: 0 var(--sp-m);
|
||||
}
|
||||
|
||||
.selected-colors {
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
{:x (.-clientX ^js event)
|
||||
:y (.-clientY ^js event)
|
||||
:on-accept edit-color
|
||||
:origin :assets
|
||||
:data color
|
||||
:position :right})))
|
||||
|
||||
@@ -407,6 +408,7 @@
|
||||
{:x x-position
|
||||
:y y-position
|
||||
:on-accept add-color
|
||||
:origin :assets
|
||||
:data {:color "#406280"
|
||||
:opacity 1}
|
||||
:position :right})))))
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.main.data.workspace.colors :as dwc]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.title-bar :refer [title-bar*]]
|
||||
[app.main.ui.hooks :as h]
|
||||
@@ -34,6 +35,18 @@
|
||||
(def xf:map-shape-id
|
||||
(map :shape-id))
|
||||
|
||||
(defn- generate-color-operations
|
||||
[groups old-color prev-colors]
|
||||
(let [old-color (-> old-color
|
||||
(dissoc :name :path)
|
||||
(d/without-nils))
|
||||
prev-color (d/seek (partial get groups) prev-colors)
|
||||
color-operations-old (get groups old-color)
|
||||
color-operations-prev (get groups prev-colors)
|
||||
color-operations (or color-operations-prev color-operations-old)
|
||||
old-color (or prev-color old-color)]
|
||||
[color-operations old-color]))
|
||||
|
||||
(mf/defc color-selection-menu*
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
|
||||
[{:keys [shapes file-id libraries]}]
|
||||
@@ -57,22 +70,13 @@
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(fn [new-color old-color from-picker?]
|
||||
(let [old-color (-> old-color
|
||||
(dissoc :name :path)
|
||||
(d/without-nils))
|
||||
|
||||
;; When dragging on the color picker sometimes all
|
||||
(let [;; When dragging on the color picker sometimes all
|
||||
;; the shapes hasn't updated the color to the prev
|
||||
;; value so we need this extra calculation
|
||||
groups (mf/ref-val groups-ref)
|
||||
prev-colors (mf/ref-val prev-colors-ref)
|
||||
|
||||
prev-color (d/seek (partial get groups) prev-colors)
|
||||
|
||||
cops-old (get groups old-color)
|
||||
cops-prev (get groups prev-colors)
|
||||
cops (or cops-prev cops-old)
|
||||
old-color (or prev-color old-color)]
|
||||
[color-operations old-color] (generate-color-operations groups old-color prev-colors)]
|
||||
|
||||
(when from-picker?
|
||||
(let [color (-> new-color
|
||||
@@ -81,7 +85,7 @@
|
||||
(mf/set-ref-val! prev-colors-ref
|
||||
(conj prev-colors color))))
|
||||
|
||||
(st/emit! (dwc/change-color-in-selected cops new-color old-color)))))
|
||||
(st/emit! (dwc/change-color-in-selected color-operations new-color old-color)))))
|
||||
|
||||
on-open
|
||||
(mf/use-fn #(mf/set-ref-val! prev-colors-ref []))
|
||||
@@ -93,17 +97,31 @@
|
||||
(mf/use-fn
|
||||
(fn [color]
|
||||
(let [groups (mf/ref-val groups-ref)
|
||||
cops (get groups color)
|
||||
color-operations (get groups color)
|
||||
color' (dissoc color :id :file-id)]
|
||||
(st/emit! (dwc/change-color-in-selected cops color' color)))))
|
||||
(st/emit! (dwc/change-color-in-selected color-operations color' color)))))
|
||||
|
||||
select-only
|
||||
(mf/use-fn
|
||||
(fn [color]
|
||||
(let [groups (mf/ref-val groups-ref)
|
||||
cops (get groups color)
|
||||
ids (into (d/ordered-set) xf:map-shape-id cops)]
|
||||
(st/emit! (dws/select-shapes ids)))))]
|
||||
color-operations (get groups color)
|
||||
ids (into (d/ordered-set) xf:map-shape-id color-operations)]
|
||||
(st/emit! (dws/select-shapes ids)))))
|
||||
on-token-change
|
||||
(mf/use-fn
|
||||
(fn [_ token old-color]
|
||||
(let [groups (mf/ref-val groups-ref)
|
||||
prev-colors (mf/ref-val prev-colors-ref)
|
||||
resolved-value (:resolved-value token)
|
||||
new-color (dwta/value->color resolved-value)
|
||||
color (-> new-color
|
||||
(dissoc :name :path)
|
||||
(d/without-nils))
|
||||
[color-operations _] (generate-color-operations groups old-color prev-colors)]
|
||||
(mf/set-ref-val! prev-colors-ref
|
||||
(conj prev-colors color))
|
||||
(st/emit! (dwta/apply-token-on-selected color-operations token)))))]
|
||||
|
||||
[:div {:class (stl/css :element-set)}
|
||||
[:div {:class (stl/css :element-title)}
|
||||
@@ -132,7 +150,9 @@
|
||||
:on-detach on-detach
|
||||
:select-only select-only
|
||||
:on-change #(on-change %1 color %2)
|
||||
:on-token-change #(on-token-change %1 %2 color)
|
||||
:on-open on-open
|
||||
:origin :color-selection-library
|
||||
:on-close on-close}]))
|
||||
(when (and (false? @expand-lib-color) (< 3 (count library-colors)))
|
||||
[:button {:class (stl/css :more-colors-btn)
|
||||
@@ -147,6 +167,8 @@
|
||||
:index index
|
||||
:select-only select-only
|
||||
:on-change #(on-change %1 color %2)
|
||||
:origin :color-selection
|
||||
:on-token-change #(on-token-change %1 %2 color)
|
||||
:on-open on-open
|
||||
:on-close on-close}])
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@
|
||||
:on-remove on-remove
|
||||
:disable-drag disable-drag?
|
||||
:on-focus on-focus
|
||||
:origin :fill
|
||||
:select-on-focus (not disable-drag?)
|
||||
:on-blur on-blur}]))])
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
:title (tr "workspace.options.grid.params.color")
|
||||
:disable-gradient true
|
||||
:disable-image true
|
||||
:origin :guides
|
||||
:on-change handle-change-color
|
||||
:on-detach handle-detach-color}]
|
||||
[:button {:class (stl/css-case :show-more-options true
|
||||
@@ -242,6 +243,7 @@
|
||||
:title (tr "workspace.options.grid.params.color")
|
||||
:disable-gradient true
|
||||
:disable-image true
|
||||
:origin :guides
|
||||
:on-change handle-change-color
|
||||
:on-detach handle-detach-color}]]]
|
||||
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
:title (tr "workspace.options.shadow-options.color")
|
||||
:disable-gradient true
|
||||
:disable-image true
|
||||
:origin :shadow
|
||||
:on-change on-update-color
|
||||
:on-detach on-detach-color
|
||||
:on-open on-open-row
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
:title (tr "workspace.options.canvas-background")
|
||||
:color color
|
||||
:on-change on-change
|
||||
:origin :canvas
|
||||
:on-open on-open
|
||||
:on-close on-close}]]]))
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
[app.main.ui.components.color-input :refer [color-input*]]
|
||||
[app.main.ui.components.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.components.reorder-handler :refer [reorder-handler*]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.formats :as fmt]
|
||||
@@ -47,10 +48,11 @@
|
||||
|
||||
(mf/defc color-row*
|
||||
[{:keys [index color class disable-gradient disable-opacity disable-image disable-picker hidden
|
||||
on-change on-reorder on-detach on-open on-close on-remove
|
||||
disable-drag on-focus on-blur select-only select-on-focus]}]
|
||||
on-change on-reorder on-detach on-open on-close on-remove origin
|
||||
disable-drag on-focus on-blur select-only select-on-focus on-token-change]}]
|
||||
(let [libraries (mf/deref refs/files)
|
||||
on-change (h/use-ref-callback on-change)
|
||||
on-token-change (h/use-ref-callback on-token-change)
|
||||
|
||||
file-id (or (:ref-file color) (:file-id color))
|
||||
color-id (or (:ref-id color) (:id color))
|
||||
@@ -70,6 +72,11 @@
|
||||
|
||||
class (if (some? class) (dm/str class " ") "")
|
||||
|
||||
active-tokens* (mf/use-ctx ctx/active-tokens-by-type)
|
||||
active-tokens (if active-tokens*
|
||||
@active-tokens*
|
||||
{})
|
||||
|
||||
opacity?
|
||||
(and (not multiple-colors?)
|
||||
(not library-color?)
|
||||
@@ -133,7 +140,7 @@
|
||||
|
||||
handle-click-color
|
||||
(mf/use-fn
|
||||
(mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open)
|
||||
(mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open active-tokens)
|
||||
(fn [color event]
|
||||
(let [color (cond
|
||||
multiple-colors?
|
||||
@@ -157,9 +164,13 @@
|
||||
:disable-image disable-image
|
||||
;; on-change second parameter means if the source is the color-picker
|
||||
:on-change #(on-change % index)
|
||||
:on-token-change on-token-change
|
||||
:on-close (fn [value opacity id file-id]
|
||||
(when on-close
|
||||
(on-close value opacity id file-id)))
|
||||
:active-tokens active-tokens
|
||||
:color-origin origin
|
||||
:origin :sidebar
|
||||
:data color}]
|
||||
|
||||
(when (fn? on-open)
|
||||
|
||||
@@ -157,10 +157,11 @@
|
||||
:on-remove on-remove
|
||||
:disable-drag disable-drag
|
||||
:on-focus on-focus
|
||||
:origin :stroke
|
||||
:select-on-focus select-on-focus
|
||||
:on-blur on-blur}]
|
||||
|
||||
;; Stroke Width, Alignment & Style
|
||||
;; Stroke Width, Alignment & Style
|
||||
[:div {:class (stl/css :stroke-options)}
|
||||
[:div {:class (stl/css :stroke-width-input-element)
|
||||
:title (tr "workspace.options.stroke-width")}
|
||||
|
||||
@@ -81,15 +81,29 @@
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dwtl/set-token-type-section-open type true)
|
||||
;; FIXME: use dom/get-client-position
|
||||
(modal/show (:key modal)
|
||||
{:x (.-clientX ^js event)
|
||||
:y (.-clientY ^js event)
|
||||
:position :right
|
||||
:fields (:fields modal)
|
||||
:title title
|
||||
:action "create"
|
||||
:token-type type}))))
|
||||
;; Normally the modal position is calculated by client-position,
|
||||
;; but in some cases it is opened programmatically (not by a user click),
|
||||
;; so we need to set its position explicitly.
|
||||
(let [pos (dom/get-client-position event)
|
||||
window-size (dom/get-window-size)
|
||||
left-sidebar (dom/get-element "left-sidebar-aside")
|
||||
x-size (dom/get-data left-sidebar "size")
|
||||
modal-size {:width 452
|
||||
:height 392}
|
||||
x (if (= 0 (:x pos))
|
||||
(- (int x-size) 30)
|
||||
(:x pos))
|
||||
y (if (= 0 (:y pos))
|
||||
(- (/ (:height window-size) 2) (/ (:height modal-size) 2))
|
||||
(:y pos))]
|
||||
(modal/show (:key modal)
|
||||
{:x x
|
||||
:y y
|
||||
:position :right
|
||||
:fields (:fields modal)
|
||||
:title title
|
||||
:action "create"
|
||||
:token-type type})))))
|
||||
|
||||
on-token-pill-click
|
||||
(mf/use-fn
|
||||
@@ -111,6 +125,7 @@
|
||||
[:> icon-button* {:on-click on-popover-open-click
|
||||
:variant "ghost"
|
||||
:icon i/add
|
||||
:id (str "add-token-button-" title)
|
||||
:aria-label (tr "workspace.tokens.add-token" title)}])]
|
||||
(when is-open
|
||||
[:& cmm/asset-section-block {:role :content}
|
||||
|
||||
@@ -286,6 +286,7 @@
|
||||
[:div {:ref dref
|
||||
:role "button"
|
||||
:data-testid "tokens-set-item"
|
||||
:id (str "token-set-item-" (str/join "/" tree-path))
|
||||
:style {"--tree-depth" tree-depth}
|
||||
:class (stl/css-case :set-item-container true
|
||||
:selected-set is-selected
|
||||
|
||||
@@ -1972,6 +1972,10 @@ msgstr "Close"
|
||||
msgid "labels.collapse"
|
||||
msgstr "Collapse"
|
||||
|
||||
#: src/app/main/ui/workspace/colorpicker.cljs
|
||||
msgid "labels.color"
|
||||
msgstr "Color"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:913
|
||||
msgid "labels.comment"
|
||||
msgstr "Comment"
|
||||
@@ -2777,6 +2781,12 @@ msgstr "Radial"
|
||||
msgid "media.solid"
|
||||
msgstr "Solid"
|
||||
|
||||
#: src/app/main/ui/workspace/colorpicker.cljs
|
||||
msgid "color-token.empty-state"
|
||||
msgstr ""
|
||||
"No available color tokens. "
|
||||
"Check active sets/themes or add new tokens."
|
||||
|
||||
#: src/app/main/data/common.cljs:128
|
||||
msgid "modals.add-shared-confirm-empty.hint"
|
||||
msgstr ""
|
||||
@@ -7850,6 +7860,10 @@ msgstr "Invalid value: Units are not allowed."
|
||||
msgid "workspace.tokens.warning-name-change"
|
||||
msgstr "Renaming this token will break any reference to its old name."
|
||||
|
||||
#: src/app/main/ui/workspace/colorpicker.cljs
|
||||
msgid "workspace.colorpicker.color-tokens"
|
||||
msgstr "Color tokens"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:145
|
||||
msgid "workspace.toolbar.assets"
|
||||
msgstr "Assets"
|
||||
|
||||
@@ -1978,6 +1978,10 @@ msgstr "Cerrar"
|
||||
msgid "labels.collapse"
|
||||
msgstr "Colapsar"
|
||||
|
||||
#: src/app/main/ui/workspace/colorpicker.cljs
|
||||
msgid "labels.color"
|
||||
msgstr "Color"
|
||||
|
||||
#: src/app/main/ui/comments.cljs:913
|
||||
msgid "labels.comment"
|
||||
msgstr "Comentario"
|
||||
@@ -2653,6 +2657,10 @@ msgstr "Visibilidad"
|
||||
msgid "labels.svg"
|
||||
msgstr "SVG"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:518
|
||||
msgid "labels.variant"
|
||||
msgstr "Variante"
|
||||
|
||||
#: src/app/main/ui/ds/product/loader.cljs:21
|
||||
msgid "loader.tips.01.message"
|
||||
msgstr ""
|
||||
@@ -2777,6 +2785,12 @@ msgstr "Radial"
|
||||
msgid "media.solid"
|
||||
msgstr "Sólido"
|
||||
|
||||
#: src/app/main/ui/workspace/colorpicker.cljs
|
||||
msgid "color-token.empty-state"
|
||||
msgstr ""
|
||||
"No hay tokens disponibles. "
|
||||
"Comprueba los sets/themes activos o crea nuevos tokens de color."
|
||||
|
||||
#: src/app/main/data/common.cljs:128
|
||||
msgid "modals.add-shared-confirm-empty.hint"
|
||||
msgstr ""
|
||||
@@ -7737,6 +7751,10 @@ msgstr "Valor no válido: No se permiten unidades."
|
||||
msgid "workspace.tokens.warning-name-change"
|
||||
msgstr "Al renombrar este token se romperán las referencias al nombre anterior"
|
||||
|
||||
#: src/app/main/ui/workspace/colorpicker.cljs
|
||||
msgid "workspace.colorpicker.color-tokens"
|
||||
msgstr "Tokens de color"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar.cljs:139, src/app/main/ui/workspace/sidebar.cljs:145
|
||||
msgid "workspace.toolbar.assets"
|
||||
msgstr "Recursos"
|
||||
|
||||
Reference in New Issue
Block a user