mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
✨ Add box shadow token
This commit is contained in:
committed by
Andrés Moya
parent
5c8b401037
commit
e681f95a70
@@ -123,6 +123,7 @@
|
|||||||
:token-color
|
:token-color
|
||||||
:token-typography-types
|
:token-typography-types
|
||||||
:token-typography-composite
|
:token-typography-composite
|
||||||
|
:token-shadow
|
||||||
:transit-readable-response
|
:transit-readable-response
|
||||||
:user-feedback
|
:user-feedback
|
||||||
;; TODO: remove this flag.
|
;; TODO: remove this flag.
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
(def token-type->dtcg-token-type
|
(def token-type->dtcg-token-type
|
||||||
{:boolean "boolean"
|
{:boolean "boolean"
|
||||||
:border-radius "borderRadius"
|
:border-radius "borderRadius"
|
||||||
|
:shadow "shadow"
|
||||||
:color "color"
|
:color "color"
|
||||||
:dimensions "dimension"
|
:dimensions "dimension"
|
||||||
:font-family "fontFamilies"
|
:font-family "fontFamilies"
|
||||||
@@ -77,7 +78,8 @@
|
|||||||
;; Allow these properties to be imported with singular key names for backwards compability
|
;; Allow these properties to be imported with singular key names for backwards compability
|
||||||
(assoc "fontWeight" :font-weight
|
(assoc "fontWeight" :font-weight
|
||||||
"fontSize" :font-size
|
"fontSize" :font-size
|
||||||
"fontFamily" :font-family)))
|
"fontFamily" :font-family
|
||||||
|
"boxShadow" :shadow)))
|
||||||
|
|
||||||
(def composite-token-type->dtcg-token-type
|
(def composite-token-type->dtcg-token-type
|
||||||
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
||||||
@@ -115,6 +117,12 @@
|
|||||||
|
|
||||||
(def border-radius-keys (schema-keys schema:border-radius))
|
(def border-radius-keys (schema-keys schema:border-radius))
|
||||||
|
|
||||||
|
(def ^:private schema:shadow
|
||||||
|
[:map {:title "ShadowTokenAttrs"}
|
||||||
|
[:shadow {:optional true} token-name-ref]])
|
||||||
|
|
||||||
|
(def shadow-keys (schema-keys schema:shadow))
|
||||||
|
|
||||||
(def ^:private schema:stroke-width
|
(def ^:private schema:stroke-width
|
||||||
[:map
|
[:map
|
||||||
[:stroke-width {:optional true} token-name-ref]])
|
[:stroke-width {:optional true} token-name-ref]])
|
||||||
@@ -271,6 +279,7 @@
|
|||||||
|
|
||||||
(def all-keys (set/union color-keys
|
(def all-keys (set/union color-keys
|
||||||
border-radius-keys
|
border-radius-keys
|
||||||
|
shadow-keys
|
||||||
stroke-width-keys
|
stroke-width-keys
|
||||||
sizing-keys
|
sizing-keys
|
||||||
opacity-keys
|
opacity-keys
|
||||||
@@ -289,6 +298,7 @@
|
|||||||
[:merge {:title "AppliedTokens"}
|
[:merge {:title "AppliedTokens"}
|
||||||
schema:tokens
|
schema:tokens
|
||||||
schema:border-radius
|
schema:border-radius
|
||||||
|
schema:shadow
|
||||||
schema:sizing
|
schema:sizing
|
||||||
schema:spacing
|
schema:spacing
|
||||||
schema:rotation
|
schema:rotation
|
||||||
@@ -334,6 +344,7 @@
|
|||||||
(font-weight-keys shape-attr) #{shape-attr :typography}
|
(font-weight-keys shape-attr) #{shape-attr :typography}
|
||||||
|
|
||||||
(border-radius-keys shape-attr) #{shape-attr}
|
(border-radius-keys shape-attr) #{shape-attr}
|
||||||
|
(shadow-keys shape-attr) #{shape-attr}
|
||||||
(sizing-keys shape-attr) #{shape-attr}
|
(sizing-keys shape-attr) #{shape-attr}
|
||||||
(opacity-keys shape-attr) #{shape-attr}
|
(opacity-keys shape-attr) #{shape-attr}
|
||||||
(spacing-keys shape-attr) #{shape-attr}
|
(spacing-keys shape-attr) #{shape-attr}
|
||||||
@@ -361,6 +372,7 @@
|
|||||||
rotation-keys
|
rotation-keys
|
||||||
sizing-keys
|
sizing-keys
|
||||||
opacity-keys
|
opacity-keys
|
||||||
|
shadow-keys
|
||||||
position-attributes))
|
position-attributes))
|
||||||
|
|
||||||
(def rect-attributes
|
(def rect-attributes
|
||||||
@@ -444,6 +456,30 @@
|
|||||||
spacing-margin-keys)]
|
spacing-margin-keys)]
|
||||||
(unapply-token-id shape layout-item-attrs)))
|
(unapply-token-id shape layout-item-attrs)))
|
||||||
|
|
||||||
|
(def tokens-by-input
|
||||||
|
"A map from input name to applicable token for that input."
|
||||||
|
{:width #{:sizing :dimensions}
|
||||||
|
:height #{:sizing :dimensions}
|
||||||
|
:max-width #{:sizing :dimensions}
|
||||||
|
:max-height #{:sizing :dimensions}
|
||||||
|
:x #{:spacing :dimensions}
|
||||||
|
:y #{:spacing :dimensions}
|
||||||
|
:rotation #{:number :rotation}
|
||||||
|
:border-radius #{:border-radius :dimensions}
|
||||||
|
:row-gap #{:spacing :dimensions}
|
||||||
|
:column-gap #{:spacing :dimensions}
|
||||||
|
:horizontal-padding #{:spacing :dimensions}
|
||||||
|
:vertical-padding #{:spacing :dimensions}
|
||||||
|
:sided-paddings #{:spacing :dimensions}
|
||||||
|
:horizontal-margin #{:spacing :dimensions}
|
||||||
|
:vertical-margin #{:spacing :dimensions}
|
||||||
|
:sided-margins #{:spacing :dimensions}
|
||||||
|
:line-height #{:line-height :number}
|
||||||
|
:font-size #{:font-size}
|
||||||
|
:letter-spacing #{:letter-spacing}
|
||||||
|
:fill #{:color}
|
||||||
|
:stroke-color #{:color}})
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; TYPOGRAPHY
|
;; TYPOGRAPHY
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@@ -514,26 +550,11 @@
|
|||||||
[token-value]
|
[token-value]
|
||||||
(string? token-value))
|
(string? token-value))
|
||||||
|
|
||||||
(def tokens-by-input
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
"A map from input name to applicable token for that input."
|
;; SHADOW
|
||||||
{:width #{:sizing :dimensions}
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
:height #{:sizing :dimensions}
|
|
||||||
:max-width #{:sizing :dimensions}
|
(defn shadow-composite-token-reference?
|
||||||
:max-height #{:sizing :dimensions}
|
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
|
||||||
:x #{:spacing :dimensions}
|
[token-value]
|
||||||
:y #{:spacing :dimensions}
|
(string? token-value))
|
||||||
:rotation #{:number :rotation}
|
|
||||||
:border-radius #{:border-radius :dimensions}
|
|
||||||
:row-gap #{:spacing :dimensions}
|
|
||||||
:column-gap #{:spacing :dimensions}
|
|
||||||
:horizontal-padding #{:spacing :dimensions}
|
|
||||||
:vertical-padding #{:spacing :dimensions}
|
|
||||||
:sided-paddings #{:spacing :dimensions}
|
|
||||||
:horizontal-margin #{:spacing :dimensions}
|
|
||||||
:vertical-margin #{:spacing :dimensions}
|
|
||||||
:sided-margins #{:spacing :dimensions}
|
|
||||||
:line-height #{:line-height :number}
|
|
||||||
:font-size #{:font-size}
|
|
||||||
:letter-spacing #{:letter-spacing}
|
|
||||||
:fill #{:color}
|
|
||||||
:stroke-color #{:color}})
|
|
||||||
|
|||||||
@@ -1552,6 +1552,46 @@ Will return a value that matches this schema:
|
|||||||
;; Reference value
|
;; Reference value
|
||||||
value))
|
value))
|
||||||
|
|
||||||
|
(defn- convert-dtcg-shadow-composite
|
||||||
|
"Convert shadow token value from DTCG format to internal format."
|
||||||
|
[value]
|
||||||
|
(let [process-shadow (fn [shadow]
|
||||||
|
(if (map? shadow)
|
||||||
|
(let [legacy-shadow-type (get "type" shadow)]
|
||||||
|
(-> shadow
|
||||||
|
(set/rename-keys {"x" :offsetX
|
||||||
|
"offsetX" :offsetX
|
||||||
|
"y" :offsetY
|
||||||
|
"offsetY" :offsetY
|
||||||
|
"blur" :blur
|
||||||
|
"spread" :spread
|
||||||
|
"color" :color
|
||||||
|
"inset" :inset})
|
||||||
|
(update :inset #(cond
|
||||||
|
(boolean? %) %
|
||||||
|
(= "true" %) true
|
||||||
|
(= "false" %) false
|
||||||
|
(= legacy-shadow-type "innerShadow") true
|
||||||
|
:else false))
|
||||||
|
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
|
||||||
|
shadow))]
|
||||||
|
(cond
|
||||||
|
;; Reference value - keep as string
|
||||||
|
(string? value)
|
||||||
|
value
|
||||||
|
|
||||||
|
;; Array of shadows - process each
|
||||||
|
(sequential? value)
|
||||||
|
(mapv process-shadow value)
|
||||||
|
|
||||||
|
;; Single shadow object - wrap in vector
|
||||||
|
(map? value)
|
||||||
|
[(process-shadow value)]
|
||||||
|
|
||||||
|
;; Fallback - keep as is
|
||||||
|
:else
|
||||||
|
value)))
|
||||||
|
|
||||||
(defn- flatten-nested-tokens-json
|
(defn- flatten-nested-tokens-json
|
||||||
"Convert a tokens tree in the decoded json fragment into a flat map,
|
"Convert a tokens tree in the decoded json fragment into a flat map,
|
||||||
being the keys the token paths after joining the keys with '.'."
|
being the keys the token paths after joining the keys with '.'."
|
||||||
@@ -1574,6 +1614,7 @@ Will return a value that matches this schema:
|
|||||||
(case token-type
|
(case token-type
|
||||||
:font-family (convert-dtcg-font-family token-value)
|
:font-family (convert-dtcg-font-family token-value)
|
||||||
:typography (convert-dtcg-typography-composite token-value)
|
:typography (convert-dtcg-typography-composite token-value)
|
||||||
|
:shadow (convert-dtcg-shadow-composite token-value)
|
||||||
token-value))
|
token-value))
|
||||||
:description (get v "$description")))
|
:description (get v "$description")))
|
||||||
;; Discard unknown type tokens
|
;; Discard unknown type tokens
|
||||||
@@ -1739,11 +1780,32 @@ Will return a value that matches this schema:
|
|||||||
{} value)
|
{} value)
|
||||||
value))
|
value))
|
||||||
|
|
||||||
|
(defn- shadow-token->dtcg-token
|
||||||
|
"Convert shadow token value from internal format to DTCG format."
|
||||||
|
[value]
|
||||||
|
(if (sequential? value)
|
||||||
|
(mapv (fn [shadow]
|
||||||
|
(if (map? shadow)
|
||||||
|
(-> shadow
|
||||||
|
(set/rename-keys {:offsetX "offsetX"
|
||||||
|
:offsetY "offsetY"
|
||||||
|
:blur "blur"
|
||||||
|
:spread "spread"
|
||||||
|
:color "color"
|
||||||
|
:inset "inset"})
|
||||||
|
(select-keys ["offsetX" "offsetY" "blur" "spread" "color" "inset"]))
|
||||||
|
shadow))
|
||||||
|
value)
|
||||||
|
value))
|
||||||
|
|
||||||
(defn- token->dtcg-token [token]
|
(defn- token->dtcg-token [token]
|
||||||
(cond-> {"$value" (cond-> (:value token)
|
(cond-> {"$value" (cond-> (:value token)
|
||||||
;; Transform typography token values
|
;; Transform typography token values
|
||||||
(= :typography (:type token))
|
(= :typography (:type token))
|
||||||
typography-token->dtcg-token)
|
typography-token->dtcg-token
|
||||||
|
;; Transform shadow token values
|
||||||
|
(= :shadow (:type token))
|
||||||
|
shadow-token->dtcg-token)
|
||||||
"$type" (cto/token-type->dtcg-token-type (:type token))}
|
"$type" (cto/token-type->dtcg-token-type (:type token))}
|
||||||
(:description token) (assoc "$description" (:description token))))
|
(:description token) (assoc "$description" (:description token))))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"test": {
|
||||||
|
"shadow-single": {
|
||||||
|
"$value": {
|
||||||
|
"x": "0",
|
||||||
|
"y": "2px",
|
||||||
|
"blur": "4px",
|
||||||
|
"spread": "0",
|
||||||
|
"color": "#000"
|
||||||
|
},
|
||||||
|
"$type": "boxShadow"
|
||||||
|
},
|
||||||
|
"shadow-multiple": {
|
||||||
|
"$value": [
|
||||||
|
{
|
||||||
|
"x": "0",
|
||||||
|
"y": "2px",
|
||||||
|
"blur": "4px",
|
||||||
|
"spread": "0",
|
||||||
|
"color": "#000",
|
||||||
|
"inset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": "0",
|
||||||
|
"y": "8px",
|
||||||
|
"blur": "16px",
|
||||||
|
"spread": "0",
|
||||||
|
"color": "#000",
|
||||||
|
"inset": "true"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"$type": "boxShadow"
|
||||||
|
},
|
||||||
|
"shadow-ref": {
|
||||||
|
"$value": "{shadow-single}",
|
||||||
|
"$type": "boxShadow"
|
||||||
|
},
|
||||||
|
"shadow-with-type": {
|
||||||
|
"$value": {
|
||||||
|
"x": "0",
|
||||||
|
"y": "4px",
|
||||||
|
"blur": "8px",
|
||||||
|
"spread": "0",
|
||||||
|
"color": "rgba(0,0,0,0.2)",
|
||||||
|
"type": "innerShadow"
|
||||||
|
},
|
||||||
|
"$type": "boxShadow"
|
||||||
|
},
|
||||||
|
"shadow-with-description": {
|
||||||
|
"$value": {
|
||||||
|
"x": "1px",
|
||||||
|
"y": "1px",
|
||||||
|
"blur": "3px",
|
||||||
|
"color": "gray"
|
||||||
|
},
|
||||||
|
"$type": "boxShadow",
|
||||||
|
"$description": "A simple shadow token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1362,9 +1362,7 @@
|
|||||||
{:name "button.primary.background"
|
{:name "button.primary.background"
|
||||||
:type :color
|
:type :color
|
||||||
:value "{accent.default}"
|
:value "{accent.default}"
|
||||||
:description ""})))
|
:description ""}))))))
|
||||||
(t/testing "invalid tokens got discarded"
|
|
||||||
(t/is (nil? (ctob/get-token-by-name lib "theme" "boxShadow.default")))))))
|
|
||||||
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(t/deftest parse-multi-set-dtcg-json
|
(t/deftest parse-multi-set-dtcg-json
|
||||||
@@ -1392,9 +1390,7 @@
|
|||||||
{:name "button.primary.background"
|
{:name "button.primary.background"
|
||||||
:type :color
|
:type :color
|
||||||
:value "{accent.default}"
|
:value "{accent.default}"
|
||||||
:description ""})))
|
:description ""}))))))
|
||||||
(t/testing "invalid tokens got discarded"
|
|
||||||
(t/is (nil? (ctob/get-token-by-name lib "theme" "boxShadow.default")))))))
|
|
||||||
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(t/deftest parse-multi-set-dtcg-json-default-team
|
(t/deftest parse-multi-set-dtcg-json-default-team
|
||||||
@@ -1893,3 +1889,130 @@
|
|||||||
(t/is (some? imported-single))
|
(t/is (some? imported-single))
|
||||||
(t/is (= (:type imported-single) (:type original-single)))
|
(t/is (= (:type imported-single) (:type original-single)))
|
||||||
(t/is (= (:value imported-single) (:value original-single))))))))
|
(t/is (= (:value imported-single) (:value original-single))))))))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(t/deftest parse-shadow-tokens
|
||||||
|
(let [json (-> (slurp "test/common_tests/types/data/tokens-shadow-example.json")
|
||||||
|
(json/decode {:key-fn identity}))
|
||||||
|
lib (ctob/parse-decoded-json json "shadow-test")]
|
||||||
|
|
||||||
|
(t/testing "single shadow token"
|
||||||
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
|
||||||
|
(t/is (some? token))
|
||||||
|
(t/is (= :shadow (:type token)))
|
||||||
|
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||||
|
(:value token)))))
|
||||||
|
|
||||||
|
(t/testing "multiple shadow token"
|
||||||
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
|
||||||
|
(t/is (some? token))
|
||||||
|
(t/is (= :shadow (:type token)))
|
||||||
|
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||||
|
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||||
|
(:value token)))))
|
||||||
|
|
||||||
|
(t/testing "shadow token with reference"
|
||||||
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-ref")]
|
||||||
|
(t/is (some? token))
|
||||||
|
(t/is (= :shadow (:type token)))
|
||||||
|
(t/is (= "{shadow-single}" (:value token)))))
|
||||||
|
|
||||||
|
(t/testing "shadow token with type"
|
||||||
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
|
||||||
|
(t/is (some? token))
|
||||||
|
(t/is (= :shadow (:type token)))
|
||||||
|
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||||
|
(:value token)))))
|
||||||
|
|
||||||
|
(t/testing "shadow token with description"
|
||||||
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-description")]
|
||||||
|
(t/is (some? token))
|
||||||
|
(t/is (= :shadow (:type token)))
|
||||||
|
(t/is (= "A simple shadow token" (:description token))))))))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(t/deftest export-shadow-tokens
|
||||||
|
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||||
|
(ctob/add-set
|
||||||
|
(ctob/make-token-set
|
||||||
|
:name "shadow-set"
|
||||||
|
:tokens {"shadow.single"
|
||||||
|
(ctob/make-token
|
||||||
|
{:name "shadow.single"
|
||||||
|
:type :shadow
|
||||||
|
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||||
|
:description "A single shadow"})
|
||||||
|
"shadow.multiple"
|
||||||
|
(ctob/make-token
|
||||||
|
{:name "shadow.multiple"
|
||||||
|
:type :shadow
|
||||||
|
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||||
|
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||||
|
"shadow.ref"
|
||||||
|
(ctob/make-token
|
||||||
|
{:name "shadow.ref"
|
||||||
|
:type :shadow
|
||||||
|
:value "{shadow.single}"})
|
||||||
|
"shadow.empty"
|
||||||
|
(ctob/make-token
|
||||||
|
{:name "shadow.empty"
|
||||||
|
:type :shadow
|
||||||
|
:value {}})})))
|
||||||
|
result (ctob/export-dtcg-json tokens-lib)
|
||||||
|
shadow-set (get result "shadow-set")]
|
||||||
|
|
||||||
|
(t/testing "single shadow token export"
|
||||||
|
(let [single-token (get-in shadow-set ["shadow" "single"])]
|
||||||
|
(t/is (= "shadow" (get single-token "$type")))
|
||||||
|
(t/is (= [{"offsetX" "0" "offsetY" "2px" "blur" "4px" "spread" "0" "color" "#0000001A"}] (get single-token "$value")))
|
||||||
|
(t/is (= "A single shadow" (get single-token "$description")))))
|
||||||
|
|
||||||
|
(t/testing "multiple shadow token export"
|
||||||
|
(let [multiple-token (get-in shadow-set ["shadow" "multiple"])]
|
||||||
|
(t/is (= "shadow" (get multiple-token "$type")))
|
||||||
|
(t/is (= [{"offsetX" "0" "offsetY" "2px" "blur" "4px" "spread" "0" "color" "#0000001A"}
|
||||||
|
{"offsetX" "0" "offsetY" "8px" "blur" "16px" "spread" "0" "color" "#0000001A"}]
|
||||||
|
(get multiple-token "$value")))))
|
||||||
|
|
||||||
|
(t/testing "reference shadow token export"
|
||||||
|
(let [ref-token (get-in shadow-set ["shadow" "ref"])]
|
||||||
|
(t/is (= "shadow" (get ref-token "$type")))
|
||||||
|
(t/is (= "{shadow.single}" (get ref-token "$value")))))
|
||||||
|
|
||||||
|
(t/testing "empty shadow token export"
|
||||||
|
(let [empty-token (get-in shadow-set ["shadow" "empty"])]
|
||||||
|
(t/is (= "shadow" (get empty-token "$type")))
|
||||||
|
(t/is (= {} (get empty-token "$value"))))))))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(t/deftest shadow-token-round-trip
|
||||||
|
(let [original-lib (-> (ctob/make-tokens-lib)
|
||||||
|
(ctob/add-set
|
||||||
|
(ctob/make-token-set
|
||||||
|
:name "test-set"
|
||||||
|
:tokens {"shadow.test"
|
||||||
|
(ctob/make-token
|
||||||
|
{:name "shadow.test"
|
||||||
|
:type :shadow
|
||||||
|
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||||
|
:description "Round trip test"})
|
||||||
|
"shadow.ref"
|
||||||
|
(ctob/make-token
|
||||||
|
{:name "shadow.ref"
|
||||||
|
:type :shadow
|
||||||
|
:value "{shadow.test}"})})))
|
||||||
|
exported (ctob/export-dtcg-json original-lib)
|
||||||
|
imported-lib (ctob/parse-decoded-json exported "")]
|
||||||
|
|
||||||
|
(t/testing "round trip preserves shadow tokens"
|
||||||
|
(let [original-token (ctob/get-token-by-name original-lib "test-set" "shadow.test")
|
||||||
|
imported-token (ctob/get-token-by-name imported-lib "test-set" "shadow.test")]
|
||||||
|
(t/is (some? imported-token))
|
||||||
|
(t/is (= (:type original-token) (:type imported-token)))
|
||||||
|
(t/is (= (:value original-token) (:value imported-token)))
|
||||||
|
(t/is (= (:description original-token) (:description imported-token))))
|
||||||
|
(let [original-ref (ctob/get-token-by-name original-lib "test-set" "shadow.ref")
|
||||||
|
imported-ref (ctob/get-token-by-name imported-lib "test-set" "shadow.ref")]
|
||||||
|
(t/is (some? imported-ref))
|
||||||
|
(t/is (= (:type original-ref) (:type imported-ref)))
|
||||||
|
(t/is (= (:value imported-ref) (:value original-ref))))))))
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ test.beforeEach(async ({ page }) => {
|
|||||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
const setupEmptyTokensFile = async (page) => {
|
const setupEmptyTokensFile = async (page, options = {}) => {
|
||||||
|
const { flags = [] } = options;
|
||||||
|
|
||||||
const workspacePage = new WorkspacePage(page);
|
const workspacePage = new WorkspacePage(page);
|
||||||
|
if (flags.length > 0) {
|
||||||
|
await workspacePage.mockConfigFlags(flags);
|
||||||
|
}
|
||||||
|
|
||||||
await workspacePage.setupEmptyFile();
|
await workspacePage.setupEmptyFile();
|
||||||
await workspacePage.mockRPC(
|
await workspacePage.mockRPC(
|
||||||
"get-team?id=*",
|
"get-team?id=*",
|
||||||
@@ -1155,6 +1161,228 @@ test.describe("Tokens: Themes modal", () => {
|
|||||||
name: newTokenTitle,
|
name: newTokenTitle,
|
||||||
});
|
});
|
||||||
await expect(newToken).toBeVisible();
|
await expect(newToken).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("User adds shadow token with multiple shadows and applies it to shape", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { tokensUpdateCreateModal, tokensSidebar, workspacePage, tokenContextMenuForToken } =
|
||||||
|
await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||||
|
|
||||||
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
|
||||||
|
await test.step("Stage 1: Basic open", async () => {
|
||||||
|
// User adds shadow via the sidebar
|
||||||
|
await tokensTabPanel
|
||||||
|
.getByRole("button", { name: "Add Token: Shadow" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
await nameField.fill("shadow.primary");
|
||||||
|
|
||||||
|
// User adds first shadow with a color from the color ramp
|
||||||
|
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
|
"shadow-input-fields-0",
|
||||||
|
);
|
||||||
|
await expect(firstShadowFields).toBeVisible();
|
||||||
|
|
||||||
|
// Fill in the shadow values
|
||||||
|
const offsetXInput = firstShadowFields.getByLabel("X");
|
||||||
|
const offsetYInput = firstShadowFields.getByLabel("Y");
|
||||||
|
const blurInput = firstShadowFields.getByLabel("Blur");
|
||||||
|
const spreadInput = firstShadowFields.getByLabel("Spread");
|
||||||
|
|
||||||
|
await offsetXInput.fill("2");
|
||||||
|
await offsetYInput.fill("2");
|
||||||
|
await blurInput.fill("4");
|
||||||
|
await spreadInput.fill("0");
|
||||||
|
|
||||||
|
// Add color using the color picker
|
||||||
|
const colorBullet = firstShadowFields.getByTestId(
|
||||||
|
"token-form-color-bullet",
|
||||||
|
);
|
||||||
|
await colorBullet.click();
|
||||||
|
|
||||||
|
// Click on the color ramp to select a color
|
||||||
|
const valueSaturationSelector = tokensUpdateCreateModal.getByTestId(
|
||||||
|
"value-saturation-selector",
|
||||||
|
);
|
||||||
|
await expect(valueSaturationSelector).toBeVisible();
|
||||||
|
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
|
||||||
|
|
||||||
|
// Verify that a color value was set
|
||||||
|
const colorInput = firstShadowFields.getByLabel("Color");
|
||||||
|
const firstColorValue = await colorInput.inputValue();
|
||||||
|
await expect(firstColorValue).toMatch(/^rgb(.*)$/);
|
||||||
|
|
||||||
|
// Wait for validation to complete
|
||||||
|
await expect(tokensUpdateCreateModal.getByText(/Resolved value:/).first()).toBeVisible();
|
||||||
|
|
||||||
|
// Save button should be enabled
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
await expect(submitButton).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Stage 2: Shadow adding/removing works", async () => {
|
||||||
|
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
|
"shadow-input-fields-0",
|
||||||
|
);
|
||||||
|
const colorInput = firstShadowFields.getByLabel("Color");
|
||||||
|
const firstColorValue = await colorInput.inputValue();
|
||||||
|
|
||||||
|
// User adds a second shadow
|
||||||
|
const addButton = firstShadowFields.getByTestId("shadow-add-button-0");
|
||||||
|
await addButton.click();
|
||||||
|
|
||||||
|
const secondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
|
"shadow-input-fields-1",
|
||||||
|
);
|
||||||
|
await expect(secondShadowFields).toBeVisible();
|
||||||
|
|
||||||
|
// User adds a third shadow
|
||||||
|
const addButton2 = secondShadowFields.getByTestId("shadow-add-button-1");
|
||||||
|
await addButton2.click();
|
||||||
|
|
||||||
|
const thirdShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
|
"shadow-input-fields-2",
|
||||||
|
);
|
||||||
|
await expect(thirdShadowFields).toBeVisible();
|
||||||
|
|
||||||
|
// User adds values for the third shadow
|
||||||
|
const thirdOffsetXInput = thirdShadowFields.getByLabel("X");
|
||||||
|
const thirdOffsetYInput = thirdShadowFields.getByLabel("Y");
|
||||||
|
const thirdBlurInput = thirdShadowFields.getByLabel("Blur");
|
||||||
|
const thirdSpreadInput = thirdShadowFields.getByLabel("Spread");
|
||||||
|
const thirdColorInput = thirdShadowFields.getByLabel("Color");
|
||||||
|
|
||||||
|
await thirdOffsetXInput.fill("10");
|
||||||
|
await thirdOffsetYInput.fill("10");
|
||||||
|
await thirdBlurInput.fill("20");
|
||||||
|
await thirdSpreadInput.fill("5");
|
||||||
|
await thirdColorInput.fill("#FF0000");
|
||||||
|
|
||||||
|
// User removes the 2nd shadow
|
||||||
|
const removeButton2 = secondShadowFields.getByTestId("shadow-remove-button-1");
|
||||||
|
await removeButton2.click();
|
||||||
|
|
||||||
|
// Verify second shadow is removed
|
||||||
|
await expect(secondShadowFields.getByTestId("shadow-add-button-3")).not.toBeVisible();
|
||||||
|
|
||||||
|
// Verify that the first shadow kept its values
|
||||||
|
const firstOffsetXValue = await firstShadowFields.getByLabel("X").inputValue();
|
||||||
|
const firstOffsetYValue = await firstShadowFields.getByLabel("Y").inputValue();
|
||||||
|
const firstBlurValue = await firstShadowFields.getByLabel("Blur").inputValue();
|
||||||
|
const firstSpreadValue = await firstShadowFields.getByLabel("Spread").inputValue();
|
||||||
|
const firstColorValueAfter = await firstShadowFields.getByLabel("Color").inputValue();
|
||||||
|
|
||||||
|
await expect(firstOffsetXValue).toBe("2");
|
||||||
|
await expect(firstOffsetYValue).toBe("2");
|
||||||
|
await expect(firstBlurValue).toBe("4");
|
||||||
|
await expect(firstSpreadValue).toBe("0");
|
||||||
|
await expect(firstColorValueAfter).toBe(firstColorValue);
|
||||||
|
|
||||||
|
// Verify that the third shadow (now second) kept its values
|
||||||
|
// After removing index 1, the third shadow becomes the second shadow at index 1
|
||||||
|
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
|
"shadow-input-fields-1",
|
||||||
|
);
|
||||||
|
await expect(newSecondShadowFields).toBeVisible();
|
||||||
|
|
||||||
|
const secondOffsetXValue = await newSecondShadowFields.getByLabel("X").inputValue();
|
||||||
|
const secondOffsetYValue = await newSecondShadowFields.getByLabel("Y").inputValue();
|
||||||
|
const secondBlurValue = await newSecondShadowFields.getByLabel("Blur").inputValue();
|
||||||
|
const secondSpreadValue = await newSecondShadowFields.getByLabel("Spread").inputValue();
|
||||||
|
const secondColorValue = await newSecondShadowFields.getByLabel("Color").inputValue();
|
||||||
|
|
||||||
|
await expect(secondOffsetXValue).toBe("10");
|
||||||
|
await expect(secondOffsetYValue).toBe("10");
|
||||||
|
await expect(secondBlurValue).toBe("20");
|
||||||
|
await expect(secondSpreadValue).toBe("5");
|
||||||
|
await expect(secondColorValue).toBe("#FF0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Stage 3: Restore when switching tabs works", async () => {
|
||||||
|
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
|
"shadow-input-fields-0",
|
||||||
|
);
|
||||||
|
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
|
"shadow-input-fields-1",
|
||||||
|
);
|
||||||
|
const colorInput = firstShadowFields.getByLabel("Color");
|
||||||
|
const firstColorValue = await colorInput.inputValue();
|
||||||
|
|
||||||
|
// Switch to reference tab
|
||||||
|
const referenceTabButton = tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||||
|
await referenceTabButton.click();
|
||||||
|
|
||||||
|
// Verify we're in reference mode - the composite fields should not be visible
|
||||||
|
await expect(firstShadowFields).not.toBeVisible();
|
||||||
|
|
||||||
|
// Switch back to composite tab
|
||||||
|
const compositeTabButton = tokensUpdateCreateModal.getByTestId("composite-opt");
|
||||||
|
await compositeTabButton.click();
|
||||||
|
|
||||||
|
// Verify that shadows are restored
|
||||||
|
await expect(firstShadowFields).toBeVisible();
|
||||||
|
await expect(newSecondShadowFields).toBeVisible();
|
||||||
|
|
||||||
|
// Verify first shadow values are still there
|
||||||
|
const restoredFirstOffsetX = await firstShadowFields.getByLabel("X").inputValue();
|
||||||
|
const restoredFirstOffsetY = await firstShadowFields.getByLabel("Y").inputValue();
|
||||||
|
const restoredFirstBlur = await firstShadowFields.getByLabel("Blur").inputValue();
|
||||||
|
const restoredFirstSpread = await firstShadowFields.getByLabel("Spread").inputValue();
|
||||||
|
const restoredFirstColor = await firstShadowFields.getByLabel("Color").inputValue();
|
||||||
|
|
||||||
|
await expect(restoredFirstOffsetX).toBe("2");
|
||||||
|
await expect(restoredFirstOffsetY).toBe("2");
|
||||||
|
await expect(restoredFirstBlur).toBe("4");
|
||||||
|
await expect(restoredFirstSpread).toBe("0");
|
||||||
|
await expect(restoredFirstColor).toBe(firstColorValue);
|
||||||
|
|
||||||
|
// Verify second shadow values are still there
|
||||||
|
const restoredSecondOffsetX = await newSecondShadowFields.getByLabel("X").inputValue();
|
||||||
|
const restoredSecondOffsetY = await newSecondShadowFields.getByLabel("Y").inputValue();
|
||||||
|
const restoredSecondBlur = await newSecondShadowFields.getByLabel("Blur").inputValue();
|
||||||
|
const restoredSecondSpread = await newSecondShadowFields.getByLabel("Spread").inputValue();
|
||||||
|
const restoredSecondColor = await newSecondShadowFields.getByLabel("Color").inputValue();
|
||||||
|
|
||||||
|
await expect(restoredSecondOffsetX).toBe("10");
|
||||||
|
await expect(restoredSecondOffsetY).toBe("10");
|
||||||
|
await expect(restoredSecondBlur).toBe("20");
|
||||||
|
await expect(restoredSecondSpread).toBe("5");
|
||||||
|
await expect(restoredSecondColor).toBe("#FF0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Stage 4: Layer application works", async () => {
|
||||||
|
// Save the token
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
await submitButton.click();
|
||||||
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
|
||||||
|
// Verify token appears in sidebar
|
||||||
|
const shadowToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: "shadow.primary",
|
||||||
|
});
|
||||||
|
await expect(shadowToken).toBeEnabled();
|
||||||
|
|
||||||
|
// Apply the shadow
|
||||||
|
await workspacePage.clickLayers();
|
||||||
|
await workspacePage.clickLeafLayer("Button");
|
||||||
|
|
||||||
|
const shadowSection = workspacePage.rightSidebar.getByText("Drop shadow");
|
||||||
|
await expect(shadowSection).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.getByRole("tab", { name: "Tokens" }).click();
|
||||||
|
await shadowToken.click();
|
||||||
|
|
||||||
|
await expect(shadowSection).toHaveCount(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -324,6 +324,106 @@
|
|||||||
(defn collect-typography-errors [token]
|
(defn collect-typography-errors [token]
|
||||||
(group-by :typography-key (:errors token)))
|
(group-by :typography-key (:errors token)))
|
||||||
|
|
||||||
|
(defn- parse-sd-token-shadow-inset
|
||||||
|
[value]
|
||||||
|
(let [references (seq (cto/find-token-value-references value))]
|
||||||
|
(cond
|
||||||
|
(boolean? value)
|
||||||
|
{:value value}
|
||||||
|
|
||||||
|
references
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)]
|
||||||
|
:references references}
|
||||||
|
|
||||||
|
:else
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow-type value)]})))
|
||||||
|
|
||||||
|
(defn- parse-sd-token-shadow-blur
|
||||||
|
"Parses shadow blur value (non-negative number)."
|
||||||
|
[value]
|
||||||
|
(let [parsed (parse-sd-token-general-value value)
|
||||||
|
valid? (and (:value parsed) (>= (:value parsed) 0))]
|
||||||
|
(cond
|
||||||
|
valid?
|
||||||
|
parsed
|
||||||
|
|
||||||
|
:else
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow-blur value)]})))
|
||||||
|
|
||||||
|
(defn- parse-sd-token-shadow-spread
|
||||||
|
"Parses shadow spread value (non-negative number)."
|
||||||
|
[value]
|
||||||
|
(let [parsed (parse-sd-token-general-value value)
|
||||||
|
valid? (and (:value parsed) (>= (:value parsed) 0))]
|
||||||
|
(cond
|
||||||
|
valid?
|
||||||
|
parsed
|
||||||
|
|
||||||
|
:else
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow-spread value)]})))
|
||||||
|
|
||||||
|
(defn- parse-single-shadow
|
||||||
|
"Parses a single shadow map with properties: x, y, blur, spread, color, type."
|
||||||
|
[shadow-map shadow-index]
|
||||||
|
(let [add-keyed-errors (fn [shadow-result k errors]
|
||||||
|
(update shadow-result :errors concat
|
||||||
|
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
||||||
|
parsers {:offsetX parse-sd-token-general-value
|
||||||
|
:offsetY parse-sd-token-general-value
|
||||||
|
:blur parse-sd-token-shadow-blur
|
||||||
|
:spread parse-sd-token-shadow-spread
|
||||||
|
:color parse-sd-token-color-value
|
||||||
|
:inset parse-sd-token-shadow-inset}
|
||||||
|
valid-shadow (reduce
|
||||||
|
(fn [acc [k v]]
|
||||||
|
(if-let [parser (get parsers k)]
|
||||||
|
(let [{:keys [errors value]} (parser v)]
|
||||||
|
(if (seq errors)
|
||||||
|
(add-keyed-errors acc k errors)
|
||||||
|
(assoc-in acc [:value k] (or value v))))
|
||||||
|
acc))
|
||||||
|
{:value {}}
|
||||||
|
shadow-map)]
|
||||||
|
valid-shadow))
|
||||||
|
|
||||||
|
(defn- parse-sd-token-shadow-value
|
||||||
|
"Parses shadow value and validates it."
|
||||||
|
[value]
|
||||||
|
(cond
|
||||||
|
;; Reference value (string)
|
||||||
|
(string? value) {:value value}
|
||||||
|
|
||||||
|
;; Empty value
|
||||||
|
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
||||||
|
|
||||||
|
;; Invalid value
|
||||||
|
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
|
||||||
|
|
||||||
|
;; Array of shadows
|
||||||
|
:else
|
||||||
|
(let [converted (js->clj value :keywordize-keys true)
|
||||||
|
;; Parse each shadow with its index
|
||||||
|
parsed-shadows (map-indexed
|
||||||
|
(fn [idx shadow-map]
|
||||||
|
(parse-single-shadow shadow-map idx))
|
||||||
|
converted)
|
||||||
|
|
||||||
|
;; Collect all errors from all shadows
|
||||||
|
all-errors (mapcat :errors parsed-shadows)
|
||||||
|
|
||||||
|
;; Collect all values from shadows that have values
|
||||||
|
all-values (into [] (keep :value parsed-shadows))]
|
||||||
|
|
||||||
|
(if (seq all-errors)
|
||||||
|
{:errors all-errors
|
||||||
|
:value all-values}
|
||||||
|
{:value all-values}))))
|
||||||
|
|
||||||
|
(defn collect-shadow-errors [token shadow-index]
|
||||||
|
(group-by :shadow-key
|
||||||
|
(filter #(= (:shadow-index %) shadow-index)
|
||||||
|
(:errors token))))
|
||||||
|
|
||||||
(defn process-sd-tokens
|
(defn process-sd-tokens
|
||||||
"Converts a StyleDictionary dictionary with resolved tokens (aka `sd-tokens`) back to clojure.
|
"Converts a StyleDictionary dictionary with resolved tokens (aka `sd-tokens`) back to clojure.
|
||||||
The `get-origin-token` argument should be a function that takes an
|
The `get-origin-token` argument should be a function that takes an
|
||||||
@@ -364,6 +464,7 @@
|
|||||||
(parse-atomic-typography-value (:type origin-token) value)
|
(parse-atomic-typography-value (:type origin-token) value)
|
||||||
(case (:type origin-token)
|
(case (:type origin-token)
|
||||||
:typography (parse-composite-typography-value value)
|
:typography (parse-composite-typography-value value)
|
||||||
|
:shadow (parse-sd-token-shadow-value value)
|
||||||
:color (parse-sd-token-color-value value)
|
:color (parse-sd-token-color-value value)
|
||||||
:opacity (parse-sd-token-opacity-value value)
|
:opacity (parse-sd-token-opacity-value value)
|
||||||
:stroke-width (parse-sd-token-stroke-width-value value)
|
:stroke-width (parse-sd-token-stroke-width-value value)
|
||||||
|
|||||||
@@ -149,6 +149,36 @@
|
|||||||
:ignore-touched true
|
:ignore-touched true
|
||||||
:changed-sub-attr [:stroke-color]}))))
|
:changed-sub-attr [:stroke-color]}))))
|
||||||
|
|
||||||
|
(defn value->shadow
|
||||||
|
"Transform a token shadow value into penpot shadow data structure"
|
||||||
|
[value]
|
||||||
|
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
|
||||||
|
{:id (random-uuid)
|
||||||
|
:hidden false
|
||||||
|
:offset-x offsetX
|
||||||
|
:offset-y offsetY
|
||||||
|
:blur blur
|
||||||
|
:color (value->color color)
|
||||||
|
:spread spread
|
||||||
|
:style
|
||||||
|
(case inset
|
||||||
|
true :inner-shadow
|
||||||
|
:drop-shadow)})
|
||||||
|
value))
|
||||||
|
|
||||||
|
(defn update-shadow
|
||||||
|
([value shape-ids attributes]
|
||||||
|
(update-shadow value shape-ids attributes nil))
|
||||||
|
([value shape-ids _attributes page-id]
|
||||||
|
(when (sequential? value)
|
||||||
|
(let [shadows (value->shadow value)]
|
||||||
|
(dwsh/update-shapes shape-ids
|
||||||
|
#(assoc % :shadow shadows)
|
||||||
|
{:reg-objects? true
|
||||||
|
:ignore-touched true
|
||||||
|
:page-id page-id
|
||||||
|
:attrs [:shadow]})))))
|
||||||
|
|
||||||
(defn update-fill-stroke
|
(defn update-fill-stroke
|
||||||
([value shape-ids attributes]
|
([value shape-ids attributes]
|
||||||
(update-fill-stroke value shape-ids attributes nil))
|
(update-fill-stroke value shape-ids attributes nil))
|
||||||
@@ -643,6 +673,14 @@
|
|||||||
:fields [{:label "Border Radius"
|
:fields [{:label "Border Radius"
|
||||||
:key :border-radius}]}}
|
:key :border-radius}]}}
|
||||||
|
|
||||||
|
:shadow
|
||||||
|
{:title "Shadow"
|
||||||
|
:attributes ctt/shadow-keys
|
||||||
|
:on-update-shape update-shadow
|
||||||
|
:modal {:key :tokens/shadow
|
||||||
|
:fields [{:label "Shadow"
|
||||||
|
:key :shadow}]}}
|
||||||
|
|
||||||
:color
|
:color
|
||||||
{:title "Color"
|
{:title "Color"
|
||||||
:attributes #{:fill}
|
:attributes #{:fill}
|
||||||
|
|||||||
@@ -96,6 +96,18 @@
|
|||||||
{:error/code :error.style-dictionary/composite-line-height-needs-font-size
|
{:error/code :error.style-dictionary/composite-line-height-needs-font-size
|
||||||
:error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size" %)}
|
:error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size" %)}
|
||||||
|
|
||||||
|
:error.style-dictionary/invalid-token-value-shadow-type
|
||||||
|
{:error/code :error.style-dictionary/invalid-token-value-shadow-type
|
||||||
|
:error/fn #(tr "workspace.tokens.invalid-shadow-type-token-value" %)}
|
||||||
|
|
||||||
|
:error.style-dictionary/invalid-token-value-shadow-blur
|
||||||
|
{:error/code :error.style-dictionary/invalid-token-value-shadow-blur
|
||||||
|
:error/fn #(tr "workspace.tokens.shadow-blur-range")}
|
||||||
|
|
||||||
|
:error.style-dictionary/invalid-token-value-shadow-spread
|
||||||
|
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
|
||||||
|
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
|
||||||
|
|
||||||
:error/unknown
|
:error/unknown
|
||||||
{:error/code :error/unknown
|
{:error/code :error/unknown
|
||||||
:error/fn #(tr "labels.unknown-error")}})
|
:error/fn #(tr "labels.unknown-error")}})
|
||||||
|
|||||||
@@ -15,7 +15,13 @@
|
|||||||
:line-height "Line Height"
|
:line-height "Line Height"
|
||||||
:letter-spacing "Letter Spacing"
|
:letter-spacing "Letter Spacing"
|
||||||
:text-case "Text Case"
|
:text-case "Text Case"
|
||||||
:text-decoration "Text Decoration"})
|
:text-decoration "Text Decoration"
|
||||||
|
:offsetX "X"
|
||||||
|
:offsetY "Y"
|
||||||
|
:blur "Blur"
|
||||||
|
:spread "Spread"
|
||||||
|
:color "Color"
|
||||||
|
:inset "Inner Shadow"})
|
||||||
|
|
||||||
(defn format-token-value
|
(defn format-token-value
|
||||||
"Converts token value of any shape to a string."
|
"Converts token value of any shape to a string."
|
||||||
@@ -26,6 +32,9 @@
|
|||||||
(str/join "\n")
|
(str/join "\n")
|
||||||
(str "\n"))
|
(str "\n"))
|
||||||
|
|
||||||
|
(and (sequential? token-value) (every? map? token-value))
|
||||||
|
(str/join "\n" (map format-token-value token-value))
|
||||||
|
|
||||||
(sequential? token-value)
|
(sequential? token-value)
|
||||||
(str/join ", " token-value)
|
(str/join ", " token-value)
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
ctt/text-case-keys dwta/update-text-case
|
ctt/text-case-keys dwta/update-text-case
|
||||||
ctt/text-decoration-keys dwta/update-text-decoration
|
ctt/text-decoration-keys dwta/update-text-decoration
|
||||||
ctt/typography-token-keys dwta/update-typography
|
ctt/typography-token-keys dwta/update-typography
|
||||||
|
ctt/shadow-keys dwta/update-shadow
|
||||||
#{:line-height} dwta/update-line-height
|
#{:line-height} dwta/update-line-height
|
||||||
|
|
||||||
;; Layout
|
;; Layout
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
(let [token-units? (contains? cf/flags :token-units)
|
(let [token-units? (contains? cf/flags :token-units)
|
||||||
token-typography-composite-types? (contains? cf/flags :token-typography-composite)
|
token-typography-composite-types? (contains? cf/flags :token-typography-composite)
|
||||||
token-typography-types? (contains? cf/flags :token-typography-types)
|
token-typography-types? (contains? cf/flags :token-typography-types)
|
||||||
|
token-shadow? (contains? cf/flags :token-shadow)
|
||||||
all-types (cond-> dwta/token-properties
|
all-types (cond-> dwta/token-properties
|
||||||
|
(not token-shadow?) (dissoc :shadow)
|
||||||
(not token-units?) (dissoc :number)
|
(not token-units?) (dissoc :number)
|
||||||
(not token-typography-composite-types?) (remove-keys ctt/typography-token-keys)
|
(not token-typography-composite-types?) (remove-keys ctt/typography-token-keys)
|
||||||
(not token-typography-types?) (remove-keys ctt/ff-typography-keys))
|
(not token-typography-types?) (remove-keys ctt/ff-typography-keys))
|
||||||
|
|||||||
@@ -279,7 +279,8 @@
|
|||||||
:r3 "Bottom Right"}
|
:r3 "Bottom Right"}
|
||||||
:hint (tr "workspace.tokens.radius")
|
:hint (tr "workspace.tokens.radius")
|
||||||
:on-update-shape-all dwta/update-shape-radius-all
|
:on-update-shape-all dwta/update-shape-radius-all
|
||||||
:on-update-shape update-shape-radius-for-corners})]
|
:on-update-shape update-shape-radius-for-corners})
|
||||||
|
shadow (partial generic-attribute-actions #{:shadow} "Shadow")]
|
||||||
{:border-radius border-radius
|
{:border-radius border-radius
|
||||||
:color (fn [context-data]
|
:color (fn [context-data]
|
||||||
(concat
|
(concat
|
||||||
@@ -303,6 +304,7 @@
|
|||||||
:text-decoration text-decoration
|
:text-decoration text-decoration
|
||||||
:font-weight font-weight
|
:font-weight font-weight
|
||||||
:typography typography
|
:typography typography
|
||||||
|
:shadow shadow
|
||||||
:dimensions (fn [context-data]
|
:dimensions (fn [context-data]
|
||||||
(-> (concat
|
(-> (concat
|
||||||
(when (seq (sizing-attribute-actions context-data)) [{:title "Sizing" :submenu :sizing}])
|
(when (seq (sizing-attribute-actions context-data)) [{:title "Sizing" :submenu :sizing}])
|
||||||
|
|||||||
@@ -214,6 +214,24 @@
|
|||||||
(when (empty? (:value token))
|
(when (empty? (:value token))
|
||||||
(wte/get-error-code :error.token/empty-input)))
|
(wte/get-error-code :error.token/empty-input)))
|
||||||
|
|
||||||
|
(defn check-shadow-token-self-reference
|
||||||
|
"Check token when any of the attributes in a shadow's value have a self-reference."
|
||||||
|
[token]
|
||||||
|
(let [token-name (:name token)
|
||||||
|
shadow-values (:value token)]
|
||||||
|
(some (fn [[shadow-idx shadow-map]]
|
||||||
|
(some (fn [[k v]]
|
||||||
|
(when-let [err (check-self-reference token-name v)]
|
||||||
|
(assoc err :shadow-key k :shadow-index shadow-idx)))
|
||||||
|
shadow-map))
|
||||||
|
(d/enumerate shadow-values))))
|
||||||
|
|
||||||
|
(defn check-empty-shadow-token [token]
|
||||||
|
(when (or (empty? (:value token))
|
||||||
|
(some (fn [shadow] (not-every? #(contains? shadow %) [:offsetX :offsetY :blur :spread :color]))
|
||||||
|
(:value token)))
|
||||||
|
(wte/get-error-code :error.token/empty-input)))
|
||||||
|
|
||||||
(defn validate-typography-token
|
(defn validate-typography-token
|
||||||
[{:keys [token-value] :as props}]
|
[{:keys [token-value] :as props}]
|
||||||
(cond
|
(cond
|
||||||
@@ -232,6 +250,27 @@
|
|||||||
check-typography-token-self-reference])
|
check-typography-token-self-reference])
|
||||||
(default-validate-token))))
|
(default-validate-token))))
|
||||||
|
|
||||||
|
(defn validate-shadow-token
|
||||||
|
[{:keys [token-value] :as props}]
|
||||||
|
(cond
|
||||||
|
;; Entering form without a value - show no error just resolve nil
|
||||||
|
(nil? token-value) (rx/of nil)
|
||||||
|
;; Validate refrence string
|
||||||
|
(cto/shadow-composite-token-reference? token-value) (default-validate-token props)
|
||||||
|
;; Validate composite token
|
||||||
|
:else
|
||||||
|
(-> props
|
||||||
|
(update :token-value (fn [value]
|
||||||
|
(->> (or value [])
|
||||||
|
(mapv (fn [shadow]
|
||||||
|
(d/update-when shadow :inset #(cond
|
||||||
|
(boolean? %) %
|
||||||
|
(= "true" %) true
|
||||||
|
:else false)))))))
|
||||||
|
(assoc :validators [check-empty-shadow-token
|
||||||
|
check-shadow-token-self-reference])
|
||||||
|
(default-validate-token))))
|
||||||
|
|
||||||
(defn use-debonced-resolve-callback
|
(defn use-debonced-resolve-callback
|
||||||
"Resolves a token values using `StyleDictionary`.
|
"Resolves a token values using `StyleDictionary`.
|
||||||
This function is debounced as the resolving might be an expensive calculation.
|
This function is debounced as the resolving might be an expensive calculation.
|
||||||
@@ -690,6 +729,13 @@
|
|||||||
(on-external-update-value (get @backup-state-ref next-tab))
|
(on-external-update-value (get @backup-state-ref next-tab))
|
||||||
(set-active-tab next-tab))))
|
(set-active-tab next-tab))))
|
||||||
|
|
||||||
|
update-composite-value
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [f]
|
||||||
|
(clear-resolve-value)
|
||||||
|
(swap! backup-state-ref f)
|
||||||
|
(on-external-update-value (get @backup-state-ref :composite))))
|
||||||
|
|
||||||
;; Store updated value in backup-state-ref
|
;; Store updated value in backup-state-ref
|
||||||
on-update-value'
|
on-update-value'
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@@ -724,7 +770,8 @@
|
|||||||
:is-reference-fn is-reference-fn})]
|
:is-reference-fn is-reference-fn})]
|
||||||
[:> composite-tab
|
[:> composite-tab
|
||||||
(mf/spread-props props {:default-value default-value
|
(mf/spread-props props {:default-value default-value
|
||||||
:on-update-value on-update-value'})])]]))
|
:on-update-value on-update-value'
|
||||||
|
:update-composite-value update-composite-value})])]]))
|
||||||
|
|
||||||
(mf/defc composite-form*
|
(mf/defc composite-form*
|
||||||
"Wrapper around form* that manages composite/reference tab state.
|
"Wrapper around form* that manages composite/reference tab state.
|
||||||
@@ -828,7 +875,8 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(let [open? (not color-ramp-open?)]
|
(let [open? (not color-ramp-open?)]
|
||||||
(reset! color-ramp-open* open?)
|
(reset! color-ramp-open* open?)
|
||||||
(on-display-colorpicker open?))))
|
(when on-display-colorpicker
|
||||||
|
(on-display-colorpicker open?)))))
|
||||||
|
|
||||||
swatch
|
swatch
|
||||||
(mf/html
|
(mf/html
|
||||||
@@ -913,6 +961,278 @@
|
|||||||
:custom-input-token-value color-picker*
|
:custom-input-token-value color-picker*
|
||||||
:custom-input-token-value-props custom-input-token-value-props})]))
|
:custom-input-token-value-props custom-input-token-value-props})]))
|
||||||
|
|
||||||
|
(mf/defc shadow-color-picker-wrapper*
|
||||||
|
"Wrapper for color-picker* that passes shadow color state from parent.
|
||||||
|
Similar to color-form* but receives color state from shadow-value-inputs*."
|
||||||
|
[{:keys [placeholder label default-value input-ref on-update-value on-external-update-value token-resolve-result shadow-color]}]
|
||||||
|
(let [;; Use the color state passed from parent (shadow-value-inputs*)
|
||||||
|
resolved-color (get token-resolve-result :resolved-value)
|
||||||
|
color (or shadow-color resolved-color default-value "")
|
||||||
|
|
||||||
|
custom-input-token-value-props
|
||||||
|
(mf/use-memo
|
||||||
|
(mf/deps color)
|
||||||
|
(fn []
|
||||||
|
{:color color}))]
|
||||||
|
|
||||||
|
[:> color-picker*
|
||||||
|
{:placeholder placeholder
|
||||||
|
:label label
|
||||||
|
:default-value default-value
|
||||||
|
:input-ref input-ref
|
||||||
|
:on-update-value on-update-value
|
||||||
|
:on-external-update-value on-external-update-value
|
||||||
|
:custom-input-token-value-props custom-input-token-value-props
|
||||||
|
:token-resolve-result token-resolve-result}]))
|
||||||
|
|
||||||
|
(def ^:private shadow-inputs
|
||||||
|
#(d/ordered-map
|
||||||
|
:offsetX
|
||||||
|
{:label (tr "workspace.tokens.shadow-x")
|
||||||
|
:placeholder (tr "workspace.tokens.shadow-x")}
|
||||||
|
:offsetY
|
||||||
|
{:label (tr "workspace.tokens.shadow-y")
|
||||||
|
:placeholder (tr "workspace.tokens.shadow-y")}
|
||||||
|
:blur
|
||||||
|
{:label (tr "workspace.tokens.shadow-blur")
|
||||||
|
:placeholder (tr "workspace.tokens.shadow-blur")}
|
||||||
|
:spread
|
||||||
|
{:label (tr "workspace.tokens.shadow-spread")
|
||||||
|
:placeholder (tr "workspace.tokens.shadow-spread")}
|
||||||
|
:color
|
||||||
|
{:label (tr "workspace.tokens.shadow-color")
|
||||||
|
:placeholder (tr "workspace.tokens.shadow-color")}
|
||||||
|
:inset
|
||||||
|
{:label (tr "workspace.tokens.shadow-inset")
|
||||||
|
:placeholder (tr "workspace.tokens.shadow-inset")}))
|
||||||
|
|
||||||
|
(mf/defc inset-type-select*
|
||||||
|
[{:keys [default-value shadow-idx label on-change]}]
|
||||||
|
(let [selected* (mf/use-state (or (str default-value) "false"))
|
||||||
|
selected (deref selected*)
|
||||||
|
|
||||||
|
on-change
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-change selected shadow-idx)
|
||||||
|
(fn [value e]
|
||||||
|
(obj/set! e "tokenValue" (if (= "true" value) true false))
|
||||||
|
(on-change e)
|
||||||
|
(reset! selected* (str value))))]
|
||||||
|
[:div {:class (stl/css :input-row)}
|
||||||
|
[:div {:class (stl/css :inset-label)} label]
|
||||||
|
[:& radio-buttons {:selected selected
|
||||||
|
:on-change on-change
|
||||||
|
:name (str "inset-select-" shadow-idx)}
|
||||||
|
[:& radio-button {:value "false"
|
||||||
|
:title "false"
|
||||||
|
:icon "❌"
|
||||||
|
:id (str "inset-default-" shadow-idx)}]
|
||||||
|
[:& radio-button {:value "true"
|
||||||
|
:title "true"
|
||||||
|
:icon "✅"
|
||||||
|
:id (str "inset-false-" shadow-idx)}]]]))
|
||||||
|
|
||||||
|
(mf/defc shadow-input*
|
||||||
|
[{:keys [default-value label placeholder shadow-idx input-type on-update-value on-external-update-value token-resolve-result errors-by-key shadow-color]}]
|
||||||
|
(let [color-input-ref (mf/use-ref)
|
||||||
|
|
||||||
|
on-change
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps shadow-idx input-type on-update-value)
|
||||||
|
(fn [e]
|
||||||
|
(-> (obj/set! e "tokenTypeAtIndex" [shadow-idx input-type])
|
||||||
|
(on-update-value))))
|
||||||
|
|
||||||
|
on-external-update-value'
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps shadow-idx input-type on-external-update-value)
|
||||||
|
(fn [v]
|
||||||
|
(on-external-update-value [shadow-idx input-type] v)))
|
||||||
|
|
||||||
|
resolved (get-in token-resolve-result [:resolved-value shadow-idx input-type])
|
||||||
|
|
||||||
|
errors (get errors-by-key input-type)
|
||||||
|
|
||||||
|
should-show? (or (some? resolved) (seq errors))
|
||||||
|
|
||||||
|
token-prop (when should-show?
|
||||||
|
(d/without-nils
|
||||||
|
{:resolved-value resolved
|
||||||
|
:errors errors}))]
|
||||||
|
(case input-type
|
||||||
|
:inset
|
||||||
|
[:> inset-type-select*
|
||||||
|
{:default-value default-value
|
||||||
|
:shadow-idx shadow-idx
|
||||||
|
:label label
|
||||||
|
:on-change on-change}]
|
||||||
|
:color
|
||||||
|
[:> shadow-color-picker-wrapper*
|
||||||
|
{:placeholder placeholder
|
||||||
|
:label label
|
||||||
|
:default-value default-value
|
||||||
|
:input-ref color-input-ref
|
||||||
|
:on-update-value on-change
|
||||||
|
:on-external-update-value on-external-update-value'
|
||||||
|
:token-resolve-result token-prop
|
||||||
|
:shadow-color shadow-color
|
||||||
|
:data-testid (str "shadow-color-input-" shadow-idx)}]
|
||||||
|
[:div {:class (stl/css :input-row)
|
||||||
|
:data-testid (str "shadow-" (name input-type) "-input-" shadow-idx)}
|
||||||
|
[:> input-token*
|
||||||
|
{:label label
|
||||||
|
:placeholder placeholder
|
||||||
|
:default-value default-value
|
||||||
|
:on-change on-change
|
||||||
|
:token-resolve-result token-prop}]])))
|
||||||
|
|
||||||
|
(mf/defc shadow-input-fields*
|
||||||
|
[{:keys [shadow shadow-idx on-remove-shadow on-add-shadow is-remove-disabled on-update-value token-resolve-result errors-by-key on-external-update-value shadow-color] :as props}]
|
||||||
|
(let [on-remove-shadow
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps shadow-idx on-remove-shadow)
|
||||||
|
#(on-remove-shadow shadow-idx))]
|
||||||
|
[:div {:data-testid (str "shadow-input-fields-" shadow-idx)}
|
||||||
|
[:> icon-button* {:icon i/add
|
||||||
|
:type "button"
|
||||||
|
:on-click on-add-shadow
|
||||||
|
:data-testid (str "shadow-add-button-" shadow-idx)
|
||||||
|
:aria-label (tr "workspace.tokens.shadow-add-shadow")}]
|
||||||
|
[:> icon-button* {:variant "ghost"
|
||||||
|
:type "button"
|
||||||
|
:icon i/remove
|
||||||
|
:on-click on-remove-shadow
|
||||||
|
:disabled is-remove-disabled
|
||||||
|
:data-testid (str "shadow-remove-button-" shadow-idx)
|
||||||
|
:aria-label (tr "workspace.tokens.shadow-remove-shadow")}]
|
||||||
|
(for [[input-type {:keys [label placeholder]}] (shadow-inputs)]
|
||||||
|
[:> shadow-input*
|
||||||
|
{:key (str input-type shadow-idx)
|
||||||
|
:input-type input-type
|
||||||
|
:label label
|
||||||
|
:placeholder placeholder
|
||||||
|
:shadow-idx shadow-idx
|
||||||
|
:default-value (get shadow input-type)
|
||||||
|
:on-update-value on-update-value
|
||||||
|
:token-resolve-result token-resolve-result
|
||||||
|
:errors-by-key errors-by-key
|
||||||
|
:on-external-update-value on-external-update-value
|
||||||
|
:shadow-color shadow-color}])]))
|
||||||
|
|
||||||
|
(mf/defc shadow-value-inputs*
|
||||||
|
[{:keys [default-value on-update-value token-resolve-result update-composite-value] :as props}]
|
||||||
|
(let [shadows* (mf/use-state (or default-value [{}]))
|
||||||
|
shadows (deref shadows*)
|
||||||
|
shadows-count (count shadows)
|
||||||
|
composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result)))
|
||||||
|
|
||||||
|
;; Maintain a map of color states for each shadow to prevent reset on add/remove
|
||||||
|
shadow-colors* (mf/use-state {})
|
||||||
|
shadow-colors (deref shadow-colors*)
|
||||||
|
|
||||||
|
;; Initialize color states for each shadow index
|
||||||
|
_ (mf/use-effect
|
||||||
|
(mf/deps shadows)
|
||||||
|
(fn []
|
||||||
|
(doseq [[idx shadow] (d/enumerate shadows)]
|
||||||
|
(when-not (contains? shadow-colors idx)
|
||||||
|
(let [resolved-color (get-in token-resolve-result [:resolved-value idx :color])
|
||||||
|
initial-color (or resolved-color (get shadow :color) "")]
|
||||||
|
(swap! shadow-colors* assoc idx initial-color))))))
|
||||||
|
|
||||||
|
;; Define on-external-update-value here where we have access to on-update-value
|
||||||
|
on-external-update-value
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps on-update-value shadow-colors*)
|
||||||
|
(fn [token-type-at-index value]
|
||||||
|
(let [[idx token-type] token-type-at-index
|
||||||
|
e (js-obj)]
|
||||||
|
;; Update shadow color state if this is a color update
|
||||||
|
(when (= token-type :color)
|
||||||
|
(swap! shadow-colors* assoc idx value))
|
||||||
|
(obj/set! e "tokenTypeAtIndex" token-type-at-index)
|
||||||
|
(obj/set! e "target" #js {:value value})
|
||||||
|
(on-update-value e))))
|
||||||
|
|
||||||
|
on-add-shadow
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps shadows update-composite-value)
|
||||||
|
(fn []
|
||||||
|
(update-composite-value
|
||||||
|
(fn [state]
|
||||||
|
(let [new-state (update state :composite (fnil conj []) {})]
|
||||||
|
(reset! shadows* (:composite new-state))
|
||||||
|
new-state)))))
|
||||||
|
|
||||||
|
on-remove-shadow
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps shadows update-composite-value)
|
||||||
|
(fn [idx]
|
||||||
|
(update-composite-value
|
||||||
|
(fn [state]
|
||||||
|
(let [new-state (update state :composite d/remove-at-index idx)]
|
||||||
|
(reset! shadows* (:composite new-state))
|
||||||
|
new-state)))))]
|
||||||
|
[:div {:class (stl/css :nested-input-row)}
|
||||||
|
(for [[shadow-idx shadow] (d/enumerate shadows)
|
||||||
|
:let [is-remove-disabled (= shadows-count 1)
|
||||||
|
key (str shadows-count shadow-idx)
|
||||||
|
errors-by-key (when composite-token?
|
||||||
|
(sd/collect-shadow-errors token-resolve-result shadow-idx))]]
|
||||||
|
[:div {:key key
|
||||||
|
:class (stl/css :nested-input-row)}
|
||||||
|
[:> shadow-input-fields*
|
||||||
|
{:is-remove-disabled is-remove-disabled
|
||||||
|
:shadow-idx shadow-idx
|
||||||
|
:on-add-shadow on-add-shadow
|
||||||
|
:on-remove-shadow on-remove-shadow
|
||||||
|
:shadow shadow
|
||||||
|
:on-update-value on-update-value
|
||||||
|
:token-resolve-result token-resolve-result
|
||||||
|
:errors-by-key errors-by-key
|
||||||
|
:on-external-update-value on-external-update-value
|
||||||
|
:shadow-color (get shadow-colors shadow-idx "")}]])]))
|
||||||
|
|
||||||
|
(mf/defc shadow-form*
|
||||||
|
[{:keys [token] :rest props}]
|
||||||
|
(let [on-get-token-value
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [e prev-composite-value]
|
||||||
|
(let [prev-composite-value (or prev-composite-value [])
|
||||||
|
[idx token-type :as token-type-at-index] (obj/get e "tokenTypeAtIndex")
|
||||||
|
input-value (case token-type
|
||||||
|
:inset (obj/get e "tokenValue")
|
||||||
|
(dom/get-target-val e))
|
||||||
|
reference-value-input? (not token-type-at-index)]
|
||||||
|
(cond
|
||||||
|
reference-value-input? input-value
|
||||||
|
(and (string? input-value) (empty? input-value)) (update prev-composite-value idx dissoc token-type)
|
||||||
|
:else (assoc-in prev-composite-value token-type-at-index input-value)))))
|
||||||
|
|
||||||
|
update-composite-backup-value
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [prev-composite-value e]
|
||||||
|
(let [[idx token-type :as token-type-at-index] (obj/get e "tokenTypeAtIndex")
|
||||||
|
token-value (case token-type
|
||||||
|
:inset (obj/get e "tokenValue")
|
||||||
|
(dom/get-target-val e))
|
||||||
|
valid? (case token-type
|
||||||
|
:inset (boolean? token-value)
|
||||||
|
(seq token-value))]
|
||||||
|
(if valid?
|
||||||
|
(assoc-in (or prev-composite-value []) token-type-at-index token-value)
|
||||||
|
;; Remove empty values so they don't retrigger validation when switching tabs
|
||||||
|
(update prev-composite-value idx dissoc token-type)))))]
|
||||||
|
[:> composite-form*
|
||||||
|
(mf/spread-props props {:token token
|
||||||
|
:composite-tab shadow-value-inputs*
|
||||||
|
:reference-icon i/text-typography
|
||||||
|
:is-reference-fn cto/typography-composite-token-reference?
|
||||||
|
:title (tr "workspace.tokens.shadow-title")
|
||||||
|
:validate-token validate-shadow-token
|
||||||
|
:on-get-token-value on-get-token-value
|
||||||
|
:update-composite-backup-value update-composite-backup-value})]))
|
||||||
|
|
||||||
(mf/defc font-selector-wrapper*
|
(mf/defc font-selector-wrapper*
|
||||||
[{:keys [font input-ref on-select-font on-close-font-selector]}]
|
[{:keys [font input-ref on-select-font on-close-font-selector]}]
|
||||||
(let [current-font* (mf/use-state (or font
|
(let [current-font* (mf/use-state (or font
|
||||||
@@ -1158,6 +1478,7 @@
|
|||||||
(case token-type'
|
(case token-type'
|
||||||
:color [:> color-form* props]
|
:color [:> color-form* props]
|
||||||
:typography [:> typography-form* props]
|
:typography [:> typography-form* props]
|
||||||
|
:shadow [:> shadow-form* props]
|
||||||
:font-family [:> font-family-form* props]
|
:font-family [:> font-family-form* props]
|
||||||
:text-case [:> text-case-form* props]
|
:text-case [:> text-case-form* props]
|
||||||
:text-decoration [:> text-decoration-form* props]
|
:text-decoration [:> text-decoration-form* props]
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
|
|
||||||
(mf/defc box-shadow-modal
|
(mf/defc box-shadow-modal
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
::mf/register-as :tokens/box-shadow}
|
::mf/register-as :tokens/shadow}
|
||||||
[properties]
|
[properties]
|
||||||
[:& token-update-create-modal properties])
|
[:& token-update-create-modal properties])
|
||||||
|
|
||||||
|
|||||||
@@ -469,6 +469,44 @@
|
|||||||
(t/is (= (:stroke-width (:applied-tokens rect-without-stroke')) (:name token-target')))
|
(t/is (= (:stroke-width (:applied-tokens rect-without-stroke')) (:name token-target')))
|
||||||
(t/is (= (get-in rect-without-stroke' [:strokes 0 :stroke-width]) 10))))))))))
|
(t/is (= (get-in rect-without-stroke' [:strokes 0 :stroke-width]) 10))))))))))
|
||||||
|
|
||||||
|
(t/deftest test-apply-shadow
|
||||||
|
(t/testing "applies shadow token and updates the shapes with shadow"
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(let [shadow-token {:name "shadow.sm"
|
||||||
|
:value [{:offsetX 10
|
||||||
|
:offsetY 10
|
||||||
|
:blur 10
|
||||||
|
:spread 10
|
||||||
|
:color "rgba(0,0,0,0.5)"
|
||||||
|
:inset false}]
|
||||||
|
:type :shadow}
|
||||||
|
file (-> (setup-file-with-tokens)
|
||||||
|
(update-in [:data :tokens-lib]
|
||||||
|
#(ctob/add-token % (cthi/id :set-a)
|
||||||
|
(ctob/make-token shadow-token))))
|
||||||
|
store (ths/setup-store file)
|
||||||
|
rect-1 (cths/get-shape file :rect-1)
|
||||||
|
events [(dwta/apply-token {:shape-ids [(:id rect-1)]
|
||||||
|
:attributes #{:shadow}
|
||||||
|
:token (toht/get-token file "shadow.sm")
|
||||||
|
:on-update-shape dwta/update-shadow})]]
|
||||||
|
(tohs/run-store-async
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-state new-state)
|
||||||
|
token-target' (toht/get-token file' "shadow.sm")
|
||||||
|
rect-1' (cths/get-shape file' :rect-1)
|
||||||
|
shadow (first (:shadow rect-1'))]
|
||||||
|
(t/testing "token got applied to rect with shadow and shape shadow got updated"
|
||||||
|
(t/is (= (:shadow (:applied-tokens rect-1')) (:name token-target')))
|
||||||
|
(t/is (= (:offset-x shadow) 10))
|
||||||
|
(t/is (= (:offset-y shadow) 10))
|
||||||
|
(t/is (= (:blur shadow) 10))
|
||||||
|
(t/is (= (:spread shadow) 10))
|
||||||
|
(t/is (= (get-in shadow [:color :color]) "#000000"))
|
||||||
|
(t/is (= (get-in shadow [:color :opacity]) 0.5))))))))))
|
||||||
|
|
||||||
(t/deftest test-apply-font-size
|
(t/deftest test-apply-font-size
|
||||||
(t/testing "applies font-size token and updates the text font-size"
|
(t/testing "applies font-size token and updates the text font-size"
|
||||||
(t/async
|
(t/async
|
||||||
|
|||||||
@@ -1,3 +1,50 @@
|
|||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1135
|
||||||
|
msgid "workspace.tokens.shadow-title"
|
||||||
|
msgstr "Shadows"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:943
|
||||||
|
msgid "workspace.tokens.shadow-x"
|
||||||
|
msgstr "X"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:946
|
||||||
|
msgid "workspace.tokens.shadow-y"
|
||||||
|
msgstr "Y"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:949
|
||||||
|
msgid "workspace.tokens.shadow-blur"
|
||||||
|
msgstr "Blur"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:952
|
||||||
|
msgid "workspace.tokens.shadow-spread"
|
||||||
|
msgstr "Spread"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:955
|
||||||
|
msgid "workspace.tokens.shadow-color"
|
||||||
|
msgstr "Color"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:958
|
||||||
|
msgid "workspace.tokens.shadow-inset"
|
||||||
|
msgstr "Inset"
|
||||||
|
|
||||||
|
#: src/app/main/data/workspace/tokens/errors.cljs:109
|
||||||
|
msgid "workspace.tokens.shadow-spread-range"
|
||||||
|
msgstr "Shadow spread must be greater than or equal to 0."
|
||||||
|
|
||||||
|
#: src/app/main/data/workspace/tokens/errors.cljs:105
|
||||||
|
msgid "workspace.tokens.shadow-blur-range"
|
||||||
|
msgstr "Shadow blur must be greater than or equal to 0."
|
||||||
|
|
||||||
|
#: src/app/main/data/workspace/tokens/errors.cljs:101
|
||||||
|
msgid "workspace.tokens.invalid-shadow-type-token-value"
|
||||||
|
msgstr "Invalid shadow type: only 'innerShadow' or 'dropShadow' are accepted"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1037
|
||||||
|
msgid "workspace.tokens.shadow-add-shadow"
|
||||||
|
msgstr "Add Shadow"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1044
|
||||||
|
msgid "workspace.tokens.shadow-remove-shadow"
|
||||||
|
msgstr "Remove Shadow"
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"PO-Revision-Date: 2025-06-09 09:13+0000\n"
|
"PO-Revision-Date: 2025-06-09 09:13+0000\n"
|
||||||
|
|||||||
@@ -1,3 +1,50 @@
|
|||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1135
|
||||||
|
msgid "workspace.tokens.shadow-title"
|
||||||
|
msgstr "Sombras"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:943
|
||||||
|
msgid "workspace.tokens.shadow-x"
|
||||||
|
msgstr "X"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:946
|
||||||
|
msgid "workspace.tokens.shadow-y"
|
||||||
|
msgstr "Y"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:949
|
||||||
|
msgid "workspace.tokens.shadow-blur"
|
||||||
|
msgstr "Blur"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:952
|
||||||
|
msgid "workspace.tokens.shadow-spread"
|
||||||
|
msgstr "Spread"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:955
|
||||||
|
msgid "workspace.tokens.shadow-color"
|
||||||
|
msgstr "Color"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:958
|
||||||
|
msgid "workspace.tokens.shadow-inset"
|
||||||
|
msgstr "Interior"
|
||||||
|
|
||||||
|
#: src/app/main/data/workspace/tokens/errors.cljs:109
|
||||||
|
msgid "workspace.tokens.shadow-spread-range"
|
||||||
|
msgstr "La extensión (spread) de la sombra debe ser mayor o igual a 0."
|
||||||
|
|
||||||
|
#: src/app/main/data/workspace/tokens/errors.cljs:105
|
||||||
|
msgid "workspace.tokens.shadow-blur-range"
|
||||||
|
msgstr "El desenfoque (blur) de la sombra debe ser mayor o igual a 0."
|
||||||
|
|
||||||
|
#: src/app/main/data/workspace/tokens/errors.cljs:101
|
||||||
|
msgid "workspace.tokens.invalid-shadow-type-token-value"
|
||||||
|
msgstr "Tipo de sombra no válida: solo se aceptan 'innerShadow' o 'dropShadow'"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1037
|
||||||
|
msgid "workspace.tokens.shadow-add-shadow"
|
||||||
|
msgstr "Añadir Sombra"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1044
|
||||||
|
msgid "workspace.tokens.shadow-remove-shadow"
|
||||||
|
msgstr "Eliminar Sombra"
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"PO-Revision-Date: 2025-10-07 16:35+0000\n"
|
"PO-Revision-Date: 2025-10-07 16:35+0000\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user