mirror of
https://github.com/penpot/penpot.git
synced 2026-09-12 23:19:28 +00:00
🎉 Add composite typography token
This commit is contained in:
parent
68a95cf0d0
commit
9106617436
@ -48,7 +48,8 @@
|
|||||||
:stroke-width "borderWidth"
|
:stroke-width "borderWidth"
|
||||||
:text-case "textCase"
|
:text-case "textCase"
|
||||||
:text-decoration "textDecoration"
|
:text-decoration "textDecoration"
|
||||||
:font-weight "fontWeights"})
|
:font-weight "fontWeights"
|
||||||
|
:typography "typography"})
|
||||||
|
|
||||||
(def dtcg-token-type->token-type
|
(def dtcg-token-type->token-type
|
||||||
(set/map-invert token-type->dtcg-token-type))
|
(set/map-invert token-type->dtcg-token-type))
|
||||||
@ -180,6 +181,12 @@
|
|||||||
|
|
||||||
(def font-weight-keys (schema-keys schema:font-weight))
|
(def font-weight-keys (schema-keys schema:font-weight))
|
||||||
|
|
||||||
|
(def ^:private schema:typography
|
||||||
|
[:map
|
||||||
|
[:typography {:optional true} token-name-ref]])
|
||||||
|
|
||||||
|
(def typography-token-keys (schema-keys schema:typography))
|
||||||
|
|
||||||
(def ^:private schema:text-decoration
|
(def ^:private schema:text-decoration
|
||||||
[:map
|
[:map
|
||||||
[:text-decoration {:optional true} token-name-ref]])
|
[:text-decoration {:optional true} token-name-ref]])
|
||||||
@ -192,7 +199,8 @@
|
|||||||
font-weight-keys
|
font-weight-keys
|
||||||
text-case-keys
|
text-case-keys
|
||||||
text-decoration-keys
|
text-decoration-keys
|
||||||
font-weight-keys))
|
font-weight-keys
|
||||||
|
typography-token-keys))
|
||||||
|
|
||||||
;; TODO: Created to extract the font-size feature from the typography feature flag.
|
;; TODO: Created to extract the font-size feature from the typography feature flag.
|
||||||
;; Delete this once the typography feature flag is removed.
|
;; Delete this once the typography feature flag is removed.
|
||||||
@ -215,6 +223,7 @@
|
|||||||
axis-keys
|
axis-keys
|
||||||
rotation-keys
|
rotation-keys
|
||||||
typography-keys
|
typography-keys
|
||||||
|
typography-token-keys
|
||||||
number-keys))
|
number-keys))
|
||||||
|
|
||||||
(def ^:private schema:tokens
|
(def ^:private schema:tokens
|
||||||
@ -260,12 +269,13 @@
|
|||||||
changed-sub-attr
|
changed-sub-attr
|
||||||
#{:m1 :m2 :m3 :m4})
|
#{:m1 :m2 :m3 :m4})
|
||||||
|
|
||||||
(font-size-keys shape-attr) #{shape-attr}
|
(font-size-keys shape-attr) #{shape-attr :typography}
|
||||||
(letter-spacing-keys shape-attr) #{shape-attr}
|
(letter-spacing-keys shape-attr) #{shape-attr :typography}
|
||||||
(font-family-keys shape-attr) #{shape-attr}
|
(font-family-keys shape-attr) #{shape-attr :typography}
|
||||||
(text-case-keys shape-attr) #{shape-attr}
|
(text-case-keys shape-attr) #{shape-attr :typography}
|
||||||
(text-decoration-keys shape-attr) #{shape-attr}
|
(text-decoration-keys shape-attr) #{shape-attr :typography}
|
||||||
(font-weight-keys shape-attr) #{shape-attr}
|
(font-weight-keys shape-attr) #{shape-attr :typography}
|
||||||
|
|
||||||
(border-radius-keys shape-attr) #{shape-attr}
|
(border-radius-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}
|
||||||
|
|||||||
@ -0,0 +1,264 @@
|
|||||||
|
{
|
||||||
|
"~:id": "~u021b87d4-813e-8066-8006-b36537098786",
|
||||||
|
"~:file-id": "~uef9b2783-804c-8017-8006-ae6f7eab52ad",
|
||||||
|
"~:created-at": "~m1756113434655",
|
||||||
|
"~:data": {
|
||||||
|
"~:objects": {
|
||||||
|
"~u00000000-0000-0000-0000-000000000000": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 0,
|
||||||
|
"~:hide-fill-on-export": false,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1.0,
|
||||||
|
"~:b": 0.0,
|
||||||
|
"~:c": 0.0,
|
||||||
|
"~:d": 1.0,
|
||||||
|
"~:e": 0.0,
|
||||||
|
"~:f": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:name": "Root Frame",
|
||||||
|
"~:width": 0.01,
|
||||||
|
"~:type": "~:frame",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.0,
|
||||||
|
"~:y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.01,
|
||||||
|
"~:y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.01,
|
||||||
|
"~:y": 0.01
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.0,
|
||||||
|
"~:y": 0.01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:r2": 0,
|
||||||
|
"~:proportion-lock": false,
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1.0,
|
||||||
|
"~:b": 0.0,
|
||||||
|
"~:c": 0.0,
|
||||||
|
"~:d": 1.0,
|
||||||
|
"~:e": 0.0,
|
||||||
|
"~:f": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:r3": 0,
|
||||||
|
"~:r1": 0,
|
||||||
|
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [],
|
||||||
|
"~:x": 0,
|
||||||
|
"~:proportion": 1.0,
|
||||||
|
"~:r4": 0,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 0,
|
||||||
|
"~:y": 0,
|
||||||
|
"~:width": 0.01,
|
||||||
|
"~:height": 0.01,
|
||||||
|
"~:x1": 0,
|
||||||
|
"~:y1": 0,
|
||||||
|
"~:x2": 0.01,
|
||||||
|
"~:y2": 0.01
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#FFFFFF",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 0.01,
|
||||||
|
"~:flip-y": null,
|
||||||
|
"~:shapes": ["~u97915939-ccca-808f-8006-aeb8eee2aa47"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~u97915939-ccca-808f-8006-aeb8eee2aa47": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 301,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1.0,
|
||||||
|
"~:b": 0.0,
|
||||||
|
"~:c": 0.0,
|
||||||
|
"~:d": 1.0,
|
||||||
|
"~:e": 0.0,
|
||||||
|
"~:f": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:grow-type": "~:auto-width",
|
||||||
|
"~:content": {
|
||||||
|
"~:type": "root",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:type": "paragraph-set",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:text-transform": "unset",
|
||||||
|
"~:text-align": "left",
|
||||||
|
"~:font-id": "gfont-open-sans",
|
||||||
|
"~:font-size": "40",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:text-direction": "ltr",
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:weight": "400",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "2",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "Open Sans",
|
||||||
|
"~:text": "Some Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:text-transform": "unset",
|
||||||
|
"~:text-align": "left",
|
||||||
|
"~:font-id": "gfont-open-sans",
|
||||||
|
"~:key": "f7ij3",
|
||||||
|
"~:font-size": "40",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:text-direction": "ltr",
|
||||||
|
"~:type": "paragraph",
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:weight": "400",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "2",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "Open Sans"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:hide-in-viewer": false,
|
||||||
|
"~:name": "Some Text",
|
||||||
|
"~:width": 214,
|
||||||
|
"~:type": "~:text",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 376,
|
||||||
|
"~:y": 301
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 590,
|
||||||
|
"~:y": 301
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 590,
|
||||||
|
"~:y": 349
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 376,
|
||||||
|
"~:y": 349
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1.0,
|
||||||
|
"~:b": 0.0,
|
||||||
|
"~:c": 0.0,
|
||||||
|
"~:d": 1.0,
|
||||||
|
"~:e": 0.0,
|
||||||
|
"~:f": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:opacity": 1,
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb8eee2aa47",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:applied-tokens": {},
|
||||||
|
"~:position-data": [
|
||||||
|
{
|
||||||
|
"~#rect": {
|
||||||
|
"~:y": 352.33333444595337,
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:font-size": "40px",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:y1": -3.3333334922790527,
|
||||||
|
"~:width": 213.5729217529297,
|
||||||
|
"~:text-decoration": "none solid rgb(0, 0, 0)",
|
||||||
|
"~:letter-spacing": "2px",
|
||||||
|
"~:x": 376,
|
||||||
|
"~:x1": 0,
|
||||||
|
"~:y2": 51.33333444595337,
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:x2": 213.5729217529297,
|
||||||
|
"~:direction": "ltr",
|
||||||
|
"~:font-family": "\"Open Sans\"",
|
||||||
|
"~:height": 54.66666793823242,
|
||||||
|
"~:text": "Some Text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:x": 376,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 376,
|
||||||
|
"~:y": 301,
|
||||||
|
"~:width": 214,
|
||||||
|
"~:height": 48,
|
||||||
|
"~:x1": 376,
|
||||||
|
"~:y1": 301,
|
||||||
|
"~:x2": 590,
|
||||||
|
"~:y2": 349
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 48,
|
||||||
|
"~:flip-y": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~uef9b2783-804c-8017-8006-ae6f7eab52ae",
|
||||||
|
"~:name": "Page 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,400 @@
|
|||||||
|
{
|
||||||
|
"~:features": {
|
||||||
|
"~#set": [
|
||||||
|
"fdata/path-data",
|
||||||
|
"plugins/runtime",
|
||||||
|
"design-tokens/v1",
|
||||||
|
"layout/grid",
|
||||||
|
"styles/v2",
|
||||||
|
"fdata/pointer-map",
|
||||||
|
"fdata/objects-map",
|
||||||
|
"components/v2",
|
||||||
|
"fdata/shape-data-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:team-id": "~ub34726bd-fd32-8122-8004-b1a4bef38917",
|
||||||
|
"~:permissions": {
|
||||||
|
"~:type": "~:membership",
|
||||||
|
"~:is-owner": true,
|
||||||
|
"~:is-admin": true,
|
||||||
|
"~:can-edit": true,
|
||||||
|
"~:can-read": true,
|
||||||
|
"~:is-logged": true
|
||||||
|
},
|
||||||
|
"~:has-media-trimmed": false,
|
||||||
|
"~:comment-thread-seqn": 0,
|
||||||
|
"~:name": "Typography",
|
||||||
|
"~:revn": 133,
|
||||||
|
"~:modified-at": "~m1756113434658",
|
||||||
|
"~:vern": 0,
|
||||||
|
"~:id": "~uef9b2783-804c-8017-8006-ae6f7eab52ad",
|
||||||
|
"~:is-shared": false,
|
||||||
|
"~:migrations": {
|
||||||
|
"~#ordered-set": []
|
||||||
|
},
|
||||||
|
"~:version": 67,
|
||||||
|
"~:project-id": "~u0df61468-6cbf-8067-8005-6b453ce996d0",
|
||||||
|
"~:created-at": "~m1755780585133",
|
||||||
|
"~:data": {
|
||||||
|
"~:pages": ["~uef9b2783-804c-8017-8006-ae6f7eab52ae"],
|
||||||
|
"~:pages-index": {
|
||||||
|
"~uef9b2783-804c-8017-8006-ae6f7eab52ae": {
|
||||||
|
"~#penpot/pointer": [
|
||||||
|
"~u021b87d4-813e-8066-8006-b36537098786",
|
||||||
|
{
|
||||||
|
"~:created-at": "~m1756113434662"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~uef9b2783-804c-8017-8006-ae6f7eab52ad",
|
||||||
|
"~:options": {
|
||||||
|
"~:components-v2": true,
|
||||||
|
"~:base-font-size": "16px"
|
||||||
|
},
|
||||||
|
"~:tokens-lib": {
|
||||||
|
"~#penpot/tokens-lib": {
|
||||||
|
"~:sets": {
|
||||||
|
"~#ordered-map": [
|
||||||
|
[
|
||||||
|
"S-Typography",
|
||||||
|
{
|
||||||
|
"~#penpot/token-set": {
|
||||||
|
"~:id": "~u7239ff52-c1d9-8014-8006-ae7ad49690e3",
|
||||||
|
"~:name": "Typography",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755800261695",
|
||||||
|
"~:tokens": {
|
||||||
|
"~#ordered-map": [
|
||||||
|
[
|
||||||
|
"42dotSans",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u7239ff52-c1d9-8014-8006-ae7ac05000f0",
|
||||||
|
"~:name": "42dotSans",
|
||||||
|
"~:type": "~:font-family",
|
||||||
|
"~:value": ["Aboreto"],
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798229771"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Full",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u7239ff52-c1d9-8014-8006-ae7ad49690e3",
|
||||||
|
"~:name": "Full",
|
||||||
|
"~:type": "~:typography",
|
||||||
|
"~:value": {
|
||||||
|
"~:font-family": ["42dot Sans"],
|
||||||
|
"~:font-size": "100",
|
||||||
|
"~:font-weight": "300",
|
||||||
|
"~:letter-spacing": "2",
|
||||||
|
"~:text-case": "uppercase",
|
||||||
|
"~:text-decoration": "underline"
|
||||||
|
},
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755800261695"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"FontSizeOnly",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb2b3f4dea3",
|
||||||
|
"~:name": "FontSizeOnly",
|
||||||
|
"~:type": "~:typography",
|
||||||
|
"~:value": {
|
||||||
|
"~:font-size": "10"
|
||||||
|
},
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798203347"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Empty",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb2b795ca5b",
|
||||||
|
"~:name": "Empty",
|
||||||
|
"~:type": "~:typography",
|
||||||
|
"~:value": {},
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798207063"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"WithAtomicReference",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb2c2049024",
|
||||||
|
"~:name": "WithAtomicReference",
|
||||||
|
"~:type": "~:typography",
|
||||||
|
"~:value": {
|
||||||
|
"~:font-size": "{Large}",
|
||||||
|
"~:font-family": ["{42dotSans}"],
|
||||||
|
"~:font-weight": "{Bold}",
|
||||||
|
"~:text-decoration": "{striked}",
|
||||||
|
"~:text-case": "{uppercase}"
|
||||||
|
},
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755800252452"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Bold",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb2d58dc828",
|
||||||
|
"~:name": "Bold",
|
||||||
|
"~:type": "~:font-weight",
|
||||||
|
"~:value": "Bold",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798237751"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Large",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb2dfd57494",
|
||||||
|
"~:name": "Large",
|
||||||
|
"~:type": "~:font-size",
|
||||||
|
"~:value": "40",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798248277"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Small",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb2ebc1d2e7",
|
||||||
|
"~:name": "Small",
|
||||||
|
"~:type": "~:font-size",
|
||||||
|
"~:value": "{Large} / 2",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798260488"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"uppercase",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb2f759c415",
|
||||||
|
"~:name": "uppercase",
|
||||||
|
"~:type": "~:text-case",
|
||||||
|
"~:value": "uppercase",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798272360"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"striked",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb305c7a666",
|
||||||
|
"~:name": "striked",
|
||||||
|
"~:type": "~:text-decoration",
|
||||||
|
"~:value": "strike-through",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798287134"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"wide",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb39796c280",
|
||||||
|
"~:name": "wide",
|
||||||
|
"~:type": "~:letter-spacing",
|
||||||
|
"~:value": "20",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798436443"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"none",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb857f7ff94",
|
||||||
|
"~:name": "none",
|
||||||
|
"~:type": "~:text-decoration",
|
||||||
|
"~:value": "none",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755799682015"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"S-Other",
|
||||||
|
{
|
||||||
|
"~#penpot/token-set": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb3c391e0f4",
|
||||||
|
"~:name": "Other",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798626309",
|
||||||
|
"~:tokens": {
|
||||||
|
"~#ordered-map": [
|
||||||
|
[
|
||||||
|
"red",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb3f86a2d7e",
|
||||||
|
"~:name": "red",
|
||||||
|
"~:type": "~:color",
|
||||||
|
"~:value": "#AA0022",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798535592"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rounded",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb412ca40b0",
|
||||||
|
"~:name": "rounded",
|
||||||
|
"~:type": "~:border-radius",
|
||||||
|
"~:value": "20px",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798562602"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dimensions",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb41bd0336c",
|
||||||
|
"~:name": "dimensions",
|
||||||
|
"~:type": "~:dimensions",
|
||||||
|
"~:value": "20",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798571840"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"number",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb4244ec6e6",
|
||||||
|
"~:name": "number",
|
||||||
|
"~:type": "~:number",
|
||||||
|
"~:value": "30",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798580539"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"transparent",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb4300eff0d",
|
||||||
|
"~:name": "transparent",
|
||||||
|
"~:type": "~:opacity",
|
||||||
|
"~:value": "0.5",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798592571"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"45deg",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb43d30e201",
|
||||||
|
"~:name": "45deg",
|
||||||
|
"~:type": "~:rotation",
|
||||||
|
"~:value": "45",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798606019"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"spacing",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb44783b70f",
|
||||||
|
"~:name": "spacing",
|
||||||
|
"~:type": "~:spacing",
|
||||||
|
"~:value": "20",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798616590"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"sizing",
|
||||||
|
{
|
||||||
|
"~#penpot/token": {
|
||||||
|
"~:id": "~u97915939-ccca-808f-8006-aeb44e0e6903",
|
||||||
|
"~:name": "sizing",
|
||||||
|
"~:type": "~:sizing",
|
||||||
|
"~:value": "20",
|
||||||
|
"~:description": "",
|
||||||
|
"~:modified-at": "~m1755798623289"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:themes": {
|
||||||
|
"~#ordered-map": [
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
"~#ordered-map": [
|
||||||
|
[
|
||||||
|
"__PENPOT__HIDDEN__TOKEN__THEME__",
|
||||||
|
{
|
||||||
|
"~#penpot/token-theme": {
|
||||||
|
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:name": "__PENPOT__HIDDEN__TOKEN__THEME__",
|
||||||
|
"~:group": "",
|
||||||
|
"~:description": "",
|
||||||
|
"~:is-source": false,
|
||||||
|
"~:external-id": "",
|
||||||
|
"~:modified-at": "~m1755798544595",
|
||||||
|
"~:sets": {
|
||||||
|
"~#set": ["Typography", "Other"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:active-themes": {
|
||||||
|
"~#set": ["/__PENPOT__HIDDEN__TOKEN__THEME__"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,18 +35,25 @@ const setupEmptyTokensFile = async (page) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupTokensFile = async (page) => {
|
const setupTokensFile = async (page, options = {}) => {
|
||||||
|
const {
|
||||||
|
file = "workspace/get-file-tokens.json",
|
||||||
|
fileFragment = "workspace/get-file-fragment-tokens.json",
|
||||||
|
flags = [],
|
||||||
|
} = options;
|
||||||
|
|
||||||
const workspacePage = new WorkspacePage(page);
|
const workspacePage = new WorkspacePage(page);
|
||||||
|
if (flags.length) {
|
||||||
|
await workspacePage.mockConfigFlags(flags);
|
||||||
|
}
|
||||||
|
|
||||||
await workspacePage.setupEmptyFile();
|
await workspacePage.setupEmptyFile();
|
||||||
await workspacePage.mockRPC(
|
await workspacePage.mockRPC(
|
||||||
"get-team?id=*",
|
"get-team?id=*",
|
||||||
"workspace/get-team-tokens.json",
|
"workspace/get-team-tokens.json",
|
||||||
);
|
);
|
||||||
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-tokens.json");
|
await workspacePage.mockRPC(/get\-file\?/, file);
|
||||||
await workspacePage.mockRPC(
|
await workspacePage.mockRPC(/get\-file\-fragment\?/, fileFragment);
|
||||||
/get\-file\-fragment\?/,
|
|
||||||
"workspace/get-file-fragment-tokens.json",
|
|
||||||
);
|
|
||||||
await workspacePage.mockRPC(
|
await workspacePage.mockRPC(
|
||||||
"update-file?id=*",
|
"update-file?id=*",
|
||||||
"workspace/update-file-create-rect.json",
|
"workspace/update-file-create-rect.json",
|
||||||
@ -73,6 +80,15 @@ const setupTokensFile = async (page) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setupTypographyTokensFile = async (page, options = {}) => {
|
||||||
|
return setupTokensFile(page, {
|
||||||
|
file: "workspace/get-file-typography-tokens.json",
|
||||||
|
fileFragment: "workspace/get-file-fragment-typography-tokens.json",
|
||||||
|
flags: ["enable-token-typography-types"],
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
test.describe("Tokens: Tokens Tab", () => {
|
test.describe("Tokens: Tokens Tab", () => {
|
||||||
test("Clicking tokens tab button opens tokens sidebar tab", async ({
|
test("Clicking tokens tab button opens tokens sidebar tab", async ({
|
||||||
page,
|
page,
|
||||||
@ -531,9 +547,7 @@ test.describe("Tokens: Sets Tab", () => {
|
|||||||
|
|
||||||
const assertEmptySetsList = async (el) => {
|
const assertEmptySetsList = async (el) => {
|
||||||
const buttons = await el.getByRole("button").allTextContents();
|
const buttons = await el.getByRole("button").allTextContents();
|
||||||
const filteredButtons = buttons.filter(
|
const filteredButtons = buttons.filter((text) => text === "Create one.");
|
||||||
(text) => text === "Create one.",
|
|
||||||
);
|
|
||||||
await expect(filteredButtons.length).toEqual(2); // We assume there are no themes, so we have two "Create one" buttons.
|
await expect(filteredButtons.length).toEqual(2); // We assume there are no themes, so we have two "Create one" buttons.
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -860,5 +874,74 @@ test.describe("Tokens: Themes modal", () => {
|
|||||||
});
|
});
|
||||||
await expect(inputColor).toHaveValue("000000");
|
await expect(inputColor).toHaveValue("000000");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User applies typography token to a text shape", async ({ page }) => {
|
||||||
|
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||||
|
await setupTypographyTokensFile(page);
|
||||||
|
|
||||||
|
await page.getByRole("tab", { name: "Layers" }).click();
|
||||||
|
|
||||||
|
await workspacePage.layers
|
||||||
|
.getByTestId("layer-row")
|
||||||
|
.filter({ hasText: "Some Text" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||||
|
await tokensTabButton.click();
|
||||||
|
|
||||||
|
await tokensSidebar
|
||||||
|
.getByRole("button")
|
||||||
|
.filter({ hasText: "Typography" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await tokensSidebar.getByRole("button", { name: "Full" }).click();
|
||||||
|
|
||||||
|
const fontSizeInput = workspacePage.rightSidebar.getByRole("textbox", {
|
||||||
|
name: "Font Size",
|
||||||
|
});
|
||||||
|
await expect(fontSizeInput).toBeVisible();
|
||||||
|
await expect(fontSizeInput).toHaveValue("100");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("User edits typography token and all fields are valid", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||||
|
await setupTypographyTokensFile(page);
|
||||||
|
|
||||||
|
await tokensSidebar
|
||||||
|
.getByRole("button")
|
||||||
|
.filter({ hasText: "Typography" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Open edit modal for "Full" typography token
|
||||||
|
const token = tokensSidebar.getByRole("button", { name: "Full" });
|
||||||
|
await token.click({ button: "right" });
|
||||||
|
await page.getByText("Edit token").click();
|
||||||
|
|
||||||
|
// Modal opens
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const saveButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: /save/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate incorrect values for font size
|
||||||
|
const fontSizeField = tokensUpdateCreateModal.getByLabel(/Font Size/i);
|
||||||
|
await fontSizeField.fill("invalid");
|
||||||
|
await expect(
|
||||||
|
tokensUpdateCreateModal.getByText(/Invalid token value:/),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(saveButton).toBeDisabled();
|
||||||
|
|
||||||
|
// Allow empty fields
|
||||||
|
await fontSizeField.fill("");
|
||||||
|
await expect(saveButton).toBeEnabled();
|
||||||
|
|
||||||
|
await saveButton.click();
|
||||||
|
|
||||||
|
// Modal should close, token should be visible (with new name) in sidebar
|
||||||
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,14 +31,21 @@
|
|||||||
Setup transforms from tokens-studio used to parse and resolved token values."
|
Setup transforms from tokens-studio used to parse and resolved token values."
|
||||||
(do
|
(do
|
||||||
(sd-transforms/register sd)
|
(sd-transforms/register sd)
|
||||||
|
(.registerTransformGroup sd #js {:name "penpot"
|
||||||
|
:transforms
|
||||||
|
;; Rebuild sd-transforms without "ts/typography/compose/shorthand" (we need to keep a typography map)
|
||||||
|
(.concat (sd-transforms/getTransforms)
|
||||||
|
#js ["ts/color/css/hexrgba"
|
||||||
|
"ts/color/modifiers"
|
||||||
|
"color/css"])})
|
||||||
(.registerFormat sd #js {:name "custom/json"
|
(.registerFormat sd #js {:name "custom/json"
|
||||||
:format (fn [^js res]
|
:format (fn [^js res]
|
||||||
(.-tokens (.-dictionary res)))})
|
(.. res -dictionary -tokens))})
|
||||||
sd))
|
sd))
|
||||||
|
|
||||||
(def default-config
|
(def default-config
|
||||||
{:platforms {:json
|
{:platforms {:json
|
||||||
{:transformGroup "tokens-studio"
|
{:transformGroup "penpot"
|
||||||
;; Required: The StyleDictionary API is focused on files even when working in the browser
|
;; Required: The StyleDictionary API is focused on files even when working in the browser
|
||||||
:files [{:format "custom/json" :destination "penpot"}]}}
|
:files [{:format "custom/json" :destination "penpot"}]}}
|
||||||
:preprocessors ["tokens-studio"]
|
:preprocessors ["tokens-studio"]
|
||||||
@ -64,7 +71,6 @@
|
|||||||
(and (string? s)
|
(and (string? s)
|
||||||
(re-matches #"^-?\d+(\.\d+)?(px|rem)$" s)))
|
(re-matches #"^-?\d+(\.\d+)?(px|rem)$" s)))
|
||||||
|
|
||||||
;; TODO: After mergin "dimension-tokens" revisit this function to check if it's still
|
|
||||||
(defn- parse-sd-token-number-value
|
(defn- parse-sd-token-number-value
|
||||||
"Parses `value` of a number `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
"Parses `value` of a number `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
|
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
|
||||||
@ -113,9 +119,9 @@
|
|||||||
"Parses `value` of a dimensions `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
"Parses `value` of a dimensions `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
||||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||||
[value has-references?]
|
[value]
|
||||||
|
(let [missing-references? (seq (ctob/find-token-value-references value))
|
||||||
(let [parsed-value (cft/parse-token-value value)
|
parsed-value (cft/parse-token-value value)
|
||||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||||
references (seq (ctob/find-token-value-references value))]
|
references (seq (ctob/find-token-value-references value))]
|
||||||
(cond (and parsed-value (not out-of-scope))
|
(cond (and parsed-value (not out-of-scope))
|
||||||
@ -125,10 +131,10 @@
|
|||||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)]
|
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)]
|
||||||
:references references}
|
:references references}
|
||||||
|
|
||||||
(and (not has-references?) out-of-scope)
|
(and (not missing-references?) out-of-scope)
|
||||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-opacity value)]}
|
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-opacity value)]}
|
||||||
|
|
||||||
(and has-references? out-of-scope parsed-value)
|
(and missing-references? out-of-scope parsed-value)
|
||||||
(assoc parsed-value :warnings [(wtw/warning-with-value :warning.style-dictionary/invalid-referenced-token-value-opacity value)])
|
(assoc parsed-value :warnings [(wtw/warning-with-value :warning.style-dictionary/invalid-referenced-token-value-opacity value)])
|
||||||
|
|
||||||
:else {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]})))
|
:else {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]})))
|
||||||
@ -137,8 +143,9 @@
|
|||||||
"Parses `value` of a dimensions `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
"Parses `value` of a dimensions `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
||||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||||
[value has-references?]
|
[value]
|
||||||
(let [parsed-value (cft/parse-token-value value)
|
(let [missing-references? (seq (ctob/find-token-value-references value))
|
||||||
|
parsed-value (cft/parse-token-value value)
|
||||||
out-of-scope (< (:value parsed-value) 0)
|
out-of-scope (< (:value parsed-value) 0)
|
||||||
references (seq (ctob/find-token-value-references value))]
|
references (seq (ctob/find-token-value-references value))]
|
||||||
(cond
|
(cond
|
||||||
@ -149,10 +156,10 @@
|
|||||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)]
|
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)]
|
||||||
:references references}
|
:references references}
|
||||||
|
|
||||||
(and (not has-references?) out-of-scope)
|
(and (not missing-references?) out-of-scope)
|
||||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-stroke-width value)]}
|
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-stroke-width value)]}
|
||||||
|
|
||||||
(and has-references? out-of-scope parsed-value)
|
(and missing-references? out-of-scope parsed-value)
|
||||||
(assoc parsed-value :warnings [(wtw/warning-with-value :warning.style-dictionary/invalid-referenced-token-value-stroke-width value)])
|
(assoc parsed-value :warnings [(wtw/warning-with-value :warning.style-dictionary/invalid-referenced-token-value-stroke-width value)])
|
||||||
|
|
||||||
:else {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]})))
|
:else {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]})))
|
||||||
@ -218,6 +225,55 @@
|
|||||||
:else
|
:else
|
||||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-font-weight value)]})))
|
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-font-weight value)]})))
|
||||||
|
|
||||||
|
(defn- parse-sd-token-font-family-value
|
||||||
|
[value]
|
||||||
|
(let [missing-references (seq (some ctob/find-token-value-references value))]
|
||||||
|
(cond
|
||||||
|
missing-references
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||||
|
:references missing-references}
|
||||||
|
|
||||||
|
:else
|
||||||
|
{:value (-> (js->clj value) (flatten))})))
|
||||||
|
|
||||||
|
(defn parse-atomic-typography-value [token-type token-value]
|
||||||
|
(case token-type
|
||||||
|
:font-size (parse-sd-token-general-value token-value)
|
||||||
|
:font-family (parse-sd-token-font-family-value token-value)
|
||||||
|
:font-weight (parse-sd-token-font-weight-value token-value)
|
||||||
|
:letter-spacing (parse-sd-token-letter-spacing-value token-value)
|
||||||
|
:text-case (parse-sd-token-text-case-value token-value)
|
||||||
|
:text-decoration (parse-sd-token-text-decoration-value token-value)
|
||||||
|
nil))
|
||||||
|
|
||||||
|
(defn- parse-composite-typography-value
|
||||||
|
[value]
|
||||||
|
(let [missing-references
|
||||||
|
(when (string? value)
|
||||||
|
(seq (ctob/find-token-value-references value)))]
|
||||||
|
(cond
|
||||||
|
missing-references
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||||
|
:references missing-references}
|
||||||
|
|
||||||
|
(string? value)
|
||||||
|
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-typography value)]}
|
||||||
|
|
||||||
|
:else
|
||||||
|
(let [converted (js->clj value :keywordize-keys true)
|
||||||
|
valid-typography (reduce
|
||||||
|
(fn [acc [k v]]
|
||||||
|
(let [{:keys [errors value]} (parse-atomic-typography-value k v)]
|
||||||
|
(if (seq errors)
|
||||||
|
(update acc :errors concat (map #(assoc % :typography-key k) errors))
|
||||||
|
(assoc-in acc [:value k] (or value v)))))
|
||||||
|
{:value {}}
|
||||||
|
converted)]
|
||||||
|
valid-typography))))
|
||||||
|
|
||||||
|
(defn collect-typography-errors [token]
|
||||||
|
(group-by :typography-key (: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
|
||||||
@ -254,18 +310,15 @@
|
|||||||
(fn [acc ^js sd-token]
|
(fn [acc ^js sd-token]
|
||||||
(let [origin-token (get-origin-token sd-token)
|
(let [origin-token (get-origin-token sd-token)
|
||||||
value (.-value sd-token)
|
value (.-value sd-token)
|
||||||
has-references? (str/includes? (:value origin-token) "{")
|
parsed-token-value (or
|
||||||
parsed-token-value (case (:type origin-token)
|
(parse-atomic-typography-value (:type origin-token) value)
|
||||||
:font-family {:value (-> (js->clj value) (flatten))}
|
(case (:type origin-token)
|
||||||
:color (parse-sd-token-color-value value)
|
:typography (parse-composite-typography-value value)
|
||||||
:opacity (parse-sd-token-opacity-value value has-references?)
|
:color (parse-sd-token-color-value value)
|
||||||
:stroke-width (parse-sd-token-stroke-width-value value has-references?)
|
:opacity (parse-sd-token-opacity-value value)
|
||||||
:text-case (parse-sd-token-text-case-value value)
|
:stroke-width (parse-sd-token-stroke-width-value value)
|
||||||
:letter-spacing (parse-sd-token-letter-spacing-value value)
|
:number (parse-sd-token-number-value value)
|
||||||
:text-decoration (parse-sd-token-text-decoration-value value)
|
(parse-sd-token-general-value value)))
|
||||||
:font-weight (parse-sd-token-font-weight-value value)
|
|
||||||
:number (parse-sd-token-number-value value)
|
|
||||||
(parse-sd-token-general-value value))
|
|
||||||
output-token (cond (:errors parsed-token-value)
|
output-token (cond (:errors parsed-token-value)
|
||||||
(merge origin-token parsed-token-value)
|
(merge origin-token parsed-token-value)
|
||||||
|
|
||||||
|
|||||||
@ -369,6 +369,33 @@
|
|||||||
:timeout 7000}))]
|
:timeout 7000}))]
|
||||||
(generate-font-weight-text-shape-update font-variant shape-ids page-id on-mismatch)))))
|
(generate-font-weight-text-shape-update font-variant shape-ids page-id on-mismatch)))))
|
||||||
|
|
||||||
|
(defn- apply-functions-map
|
||||||
|
"Apply map of functions `fs` to a map of values `vs` using `args`.
|
||||||
|
The keys for both must match to be applied with an non-nil value in `vs`.
|
||||||
|
Returns a vector of the resulting values.
|
||||||
|
|
||||||
|
E.g.: `(apply-functions {:a + :b -} {:a 1 :b nil :c 10} [1 1]) => [3]`"
|
||||||
|
[fs vs args]
|
||||||
|
(map (fn [[k f]]
|
||||||
|
(when-let [v (get vs k)]
|
||||||
|
(apply f v args)))
|
||||||
|
fs))
|
||||||
|
|
||||||
|
(defn update-typography
|
||||||
|
([value shape-ids attributes] (update-typography value shape-ids attributes nil))
|
||||||
|
([value shape-ids attributes page-id]
|
||||||
|
(when (map? value)
|
||||||
|
(rx/merge
|
||||||
|
(apply-functions-map
|
||||||
|
{:font-size update-font-size
|
||||||
|
:font-family update-font-family
|
||||||
|
:font-weight update-font-weight
|
||||||
|
:letter-spacing update-letter-spacing
|
||||||
|
:text-case update-text-case
|
||||||
|
:text-decoration update-text-decoration}
|
||||||
|
value
|
||||||
|
[shape-ids attributes page-id])))))
|
||||||
|
|
||||||
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
|
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
|
||||||
|
|
||||||
(defn apply-token
|
(defn apply-token
|
||||||
@ -383,42 +410,53 @@
|
|||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
;; We do not allow to apply tokens while text editor is open.
|
;; We do not allow to apply tokens while text editor is open.
|
||||||
(when (empty? (get state :workspace-editor-state))
|
(when (empty? (get state :workspace-editor-state))
|
||||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
(let [attributes-to-remove
|
||||||
(get :tokens-lib)
|
;; Remove atomic typography tokens when applying composite and vice-verca
|
||||||
(ctob/get-tokens-in-active-sets))]
|
(cond
|
||||||
(->> (sd/resolve-tokens tokens)
|
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
||||||
(rx/mapcat
|
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
||||||
(fn [resolved-tokens]
|
:else attributes-to-remove)]
|
||||||
(let [undo-id (js/Symbol)
|
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||||
objects (dsh/lookup-page-objects state)
|
(get :tokens-lib)
|
||||||
selected-shapes (select-keys objects shape-ids)
|
(ctob/get-tokens-in-active-sets))]
|
||||||
|
(->> (sd/resolve-tokens tokens)
|
||||||
|
(rx/mapcat
|
||||||
|
(fn [resolved-tokens]
|
||||||
|
(let [undo-id (js/Symbol)
|
||||||
|
objects (dsh/lookup-page-objects state)
|
||||||
|
selected-shapes (select-keys objects shape-ids)
|
||||||
|
|
||||||
shape-ids (or (->> selected-shapes
|
shape-ids (or (->> selected-shapes
|
||||||
(filter (fn [[_ shape]]
|
(filter (fn [[_ shape]]
|
||||||
(or
|
(or
|
||||||
(and (ctsl/any-layout-immediate-child? objects shape)
|
(and (ctsl/any-layout-immediate-child? objects shape)
|
||||||
(some ctt/spacing-margin-keys attributes))
|
(some ctt/spacing-margin-keys attributes))
|
||||||
(ctt/any-appliable-attr? attributes (:type shape)))))
|
(ctt/any-appliable-attr? attributes (:type shape)))))
|
||||||
(keys))
|
(keys))
|
||||||
[])
|
[])
|
||||||
|
|
||||||
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
|
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
|
||||||
tokenized-attributes (cft/attributes-map attributes token)
|
tokenized-attributes (cft/attributes-map attributes token)
|
||||||
type (:type token)]
|
type (:type token)]
|
||||||
(rx/of
|
(rx/concat
|
||||||
(st/emit! (ev/event {::ev/name "apply-tokens"
|
(rx/of
|
||||||
:type type
|
(st/emit! (ev/event {::ev/name "apply-tokens"
|
||||||
:applyed-to attributes}))
|
:type type
|
||||||
(dwu/start-undo-transaction undo-id)
|
:applyed-to attributes}))
|
||||||
(dwsh/update-shapes shape-ids (fn [shape]
|
(dwu/start-undo-transaction undo-id)
|
||||||
(cond-> shape
|
(dwsh/update-shapes shape-ids (fn [shape]
|
||||||
attributes-to-remove
|
(cond-> shape
|
||||||
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
attributes-to-remove
|
||||||
:always
|
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||||
(update :applied-tokens merge tokenized-attributes))))
|
:always
|
||||||
(when on-update-shape
|
(update :applied-tokens merge tokenized-attributes)))))
|
||||||
(on-update-shape resolved-value shape-ids attributes))
|
(when on-update-shape
|
||||||
(dwu/commit-undo-transaction undo-id)))))))))))
|
(let [res (on-update-shape resolved-value shape-ids attributes)]
|
||||||
|
;; Composed updates return observables and need to be executed differently
|
||||||
|
(if (rx/observable? res)
|
||||||
|
res
|
||||||
|
(rx/of res))))
|
||||||
|
(rx/of (dwu/commit-undo-transaction undo-id)))))))))))))
|
||||||
|
|
||||||
(defn apply-spacing-token
|
(defn apply-spacing-token
|
||||||
"Handles edge-case for spacing token when applying token via toggle button.
|
"Handles edge-case for spacing token when applying token via toggle button.
|
||||||
@ -554,6 +592,14 @@
|
|||||||
:fields [{:label "Font Weight"
|
:fields [{:label "Font Weight"
|
||||||
:key :font-weight}]}}
|
:key :font-weight}]}}
|
||||||
|
|
||||||
|
:typography
|
||||||
|
{:title "Typography"
|
||||||
|
:attributes ctt/typography-token-keys
|
||||||
|
:on-update-shape update-typography
|
||||||
|
:modal {:key :tokens/typography
|
||||||
|
:fields [{:label "Typography"
|
||||||
|
:key :typography}]}}
|
||||||
|
|
||||||
:text-decoration
|
:text-decoration
|
||||||
{:title "Text Decoration"
|
{:title "Text Decoration"
|
||||||
:attributes ctt/text-decoration-keys
|
:attributes ctt/text-decoration-keys
|
||||||
|
|||||||
@ -39,6 +39,7 @@
|
|||||||
#{:text-case} dwta/update-text-case
|
#{:text-case} dwta/update-text-case
|
||||||
#{:text-decoration} dwta/update-text-decoration
|
#{:text-decoration} dwta/update-text-decoration
|
||||||
#{:font-weight} dwta/update-font-weight
|
#{:font-weight} dwta/update-font-weight
|
||||||
|
#{:typography} dwta/update-typography
|
||||||
#{:x :y} dwta/update-shape-position
|
#{:x :y} dwta/update-shape-position
|
||||||
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding
|
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding
|
||||||
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin
|
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin
|
||||||
@ -161,7 +162,10 @@
|
|||||||
(collect-shapes-update-info resolved-tokens (:objects page))
|
(collect-shapes-update-info resolved-tokens (:objects page))
|
||||||
|
|
||||||
actions
|
actions
|
||||||
(actionize-shapes-update-info page-id attrs)]
|
(actionize-shapes-update-info page-id attrs)
|
||||||
|
|
||||||
|
;; Composed updates return observables and need to be executed differently
|
||||||
|
{:keys [observable normal]} (group-by #(if (rx/observable? %) :observable :normal) actions)]
|
||||||
|
|
||||||
(l/inf :status "PROGRESS"
|
(l/inf :status "PROGRESS"
|
||||||
:hint "propagate-tokens"
|
:hint "propagate-tokens"
|
||||||
@ -170,7 +174,9 @@
|
|||||||
::l/sync? true)
|
::l/sync? true)
|
||||||
|
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(rx/from actions)
|
(when (seq observable) (apply rx/merge observable))
|
||||||
|
(when (seq normal) (rx/concat-all (rx/of normal)))
|
||||||
|
|
||||||
(->> (rx/from frame-ids)
|
(->> (rx/from frame-ids)
|
||||||
(rx/mapcat (fn [frame-id]
|
(rx/mapcat (fn [frame-id]
|
||||||
(rx/of (dwt/clear-thumbnail file-id page-id frame-id "frame")
|
(rx/of (dwt/clear-thumbnail file-id page-id frame-id "frame")
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
[app.main.data.workspace.tokens.errors :as wte]
|
[app.main.data.workspace.tokens.errors :as wte]
|
||||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||||
[app.main.data.workspace.tokens.propagation :as dwtp]
|
[app.main.data.workspace.tokens.propagation :as dwtp]
|
||||||
[app.main.data.workspace.tokens.warnings :as wtw]
|
|
||||||
[app.main.fonts :as fonts]
|
[app.main.fonts :as fonts]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
@ -36,11 +35,13 @@
|
|||||||
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
|
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
|
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
|
||||||
[app.main.ui.workspace.tokens.management.create.input-token-color-bullet :refer [input-token-color-bullet*]]
|
[app.main.ui.workspace.tokens.management.create.input-token-color-bullet :refer [input-token-color-bullet*]]
|
||||||
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-tokens-value*]]
|
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-tokens-value*
|
||||||
|
token-value-hint*]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.functions :as uf]
|
[app.util.functions :as uf]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
[app.util.keyboard :as k]
|
[app.util.keyboard :as k]
|
||||||
|
[app.util.object :as obj]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[malli.core :as m]
|
[malli.core :as m]
|
||||||
@ -101,53 +102,127 @@
|
|||||||
|
|
||||||
;; Validation ------------------------------------------------------------------
|
;; Validation ------------------------------------------------------------------
|
||||||
|
|
||||||
(defn validate-empty-input [value]
|
(defn invalidate-empty-value [token-value]
|
||||||
(if (sequential? value)
|
(when (empty? (str/trim token-value))
|
||||||
(empty? value)
|
(wte/get-error-code :error.token/empty-input)))
|
||||||
(empty? (str/trim value))))
|
|
||||||
|
|
||||||
(defn validate-self-reference? [token-name value]
|
(defn invalidate-token-empty-value [token]
|
||||||
(if (sequential? value)
|
(invalidate-empty-value (:value token)))
|
||||||
(some #(ctob/token-value-self-reference? token-name %) value)
|
|
||||||
(ctob/token-value-self-reference? token-name value)))
|
|
||||||
|
|
||||||
(defn validate-token-value
|
(defn invalidate-self-reference [token-name token-value]
|
||||||
"Validates token value by resolving the value `input` using `StyleDictionary`.
|
(when (ctob/token-value-self-reference? token-name token-value)
|
||||||
Returns a promise of either resolved tokens or rejects with an error state."
|
(wte/get-error-code :error.token/direct-self-reference)))
|
||||||
[{:keys [value name-value token tokens]}]
|
|
||||||
(let [;; When creating a new token we dont have a token name yet,
|
|
||||||
;; so we use a temporary token name that hopefully doesn't clash with any of the users token names
|
|
||||||
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)]
|
|
||||||
(cond
|
|
||||||
(validate-empty-input value)
|
|
||||||
(rx/throw {:errors [(wte/get-error-code :error.token/empty-input)]})
|
|
||||||
|
|
||||||
(validate-self-reference? token-name value)
|
(defn invalidate-token-self-reference [token]
|
||||||
(rx/throw {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
|
(invalidate-self-reference (:name token) (:value token)))
|
||||||
|
|
||||||
:else
|
(defn validate-resolve-token
|
||||||
(let [tokens' (cond-> tokens
|
[token prev-token tokens]
|
||||||
;; Remove previous token when renaming a token
|
(let [token (cond-> token
|
||||||
(not= name-value (:name token)) (dissoc (:name token))
|
;; When creating a new token we dont have a name yet,
|
||||||
:always (update token-name #(ctob/make-token (merge % {:value (cond
|
;; but we still want to resolve the value to show in the form.
|
||||||
(= (:type token) :font-family) (ctt/split-font-family value)
|
;; So we use a temporary token name that hopefully doesn't clash with any of the users token names
|
||||||
:else value)
|
(str/empty? (:name token)) (assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"))
|
||||||
:name token-name
|
tokens' (cond-> tokens
|
||||||
:type (:type token)}))))]
|
;; Remove previous token when renaming a token
|
||||||
(->> tokens'
|
(not= (:name token) (:name prev-token))
|
||||||
(sd/resolve-tokens-interactive)
|
(dissoc (:name prev-token))
|
||||||
(rx/mapcat
|
|
||||||
(fn [resolved-tokens]
|
:always
|
||||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
|
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||||
(cond
|
(->> tokens'
|
||||||
resolved-value (rx/of resolved-token)
|
(sd/resolve-tokens-interactive)
|
||||||
:else (rx/throw {:errors (or errors (wte/get-error-code :error/unknown-error))}))))))))))
|
(rx/mapcat
|
||||||
|
(fn [resolved-tokens]
|
||||||
|
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
|
||||||
|
(cond
|
||||||
|
resolved-value (rx/of resolved-token)
|
||||||
|
:else (rx/throw {:errors (or (seq errors)
|
||||||
|
[(wte/get-error-code :error/unknown-error)])}))))))))
|
||||||
|
|
||||||
|
(defn validate-token-with [token validators]
|
||||||
|
(if-let [error (some (fn [validate] (validate token)) validators)]
|
||||||
|
(rx/throw {:errors [error]})
|
||||||
|
(rx/of token)))
|
||||||
|
|
||||||
|
(def default-validators
|
||||||
|
[invalidate-token-empty-value invalidate-token-self-reference])
|
||||||
|
|
||||||
|
(defn default-validate-token
|
||||||
|
"Validates a token by confirming a list of `validator` predicates and resolving the token using `tokens` with StyleDictionary.
|
||||||
|
Returns rx stream of either a valid resolved token or an errors map.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
token-name, token-value, token-description: Values from the form inputs
|
||||||
|
prev-token: The existing token currently being edited
|
||||||
|
tokens: tokens map keyed by token-name
|
||||||
|
Used to look up the editing token & resolving step.
|
||||||
|
|
||||||
|
validators: A list of predicates that will be used to do simple validation on the unresolved token map.
|
||||||
|
The validators get the token map as input and should either return:
|
||||||
|
- An errors map .e.g: {:errors []}
|
||||||
|
- nil (valid token predicate)
|
||||||
|
Mostly used to do simple checks like invalidating empy token `:name`.
|
||||||
|
Will default to `default-validators`."
|
||||||
|
[{:keys [token-name token-value token-description prev-token tokens validators]
|
||||||
|
:or {validators default-validators}}]
|
||||||
|
(let [token (-> {:name token-name
|
||||||
|
:value token-value
|
||||||
|
:description token-description}
|
||||||
|
(d/without-nils))]
|
||||||
|
(->> (rx/of token)
|
||||||
|
;; Simple validation of the editing token
|
||||||
|
(rx/mapcat #(validate-token-with % validators))
|
||||||
|
;; Resolving token via StyleDictionary
|
||||||
|
(rx/mapcat #(validate-resolve-token % prev-token tokens)))))
|
||||||
|
|
||||||
|
(defn invalidate-coll-self-reference
|
||||||
|
"Invalidate a collection of `token-vals` for a self-refernce against `token-name`.,"
|
||||||
|
[token-name token-vals]
|
||||||
|
(when (some #(ctob/token-value-self-reference? token-name %) token-vals)
|
||||||
|
(wte/get-error-code :error.token/direct-self-reference)))
|
||||||
|
|
||||||
|
(defn invalidate-font-family-token-self-reference [token]
|
||||||
|
(invalidate-coll-self-reference (:name token) (:value token)))
|
||||||
|
|
||||||
|
(defn validate-font-family-token
|
||||||
|
[props]
|
||||||
|
(-> props
|
||||||
|
(update :token-value ctt/split-font-family)
|
||||||
|
(assoc :validators [(fn [token]
|
||||||
|
(when (empty? (:value token))
|
||||||
|
(wte/get-error-code :error.token/empty-input)))
|
||||||
|
invalidate-font-family-token-self-reference])
|
||||||
|
(default-validate-token)))
|
||||||
|
|
||||||
|
(defn invalidate-typography-token-self-reference
|
||||||
|
"Invalidate token when any of the attributes in token value have a self refernce."
|
||||||
|
[token]
|
||||||
|
(let [token-name (:name token)
|
||||||
|
token-values (:value token)]
|
||||||
|
(some (fn [[k v]]
|
||||||
|
(when-let [err (case k
|
||||||
|
:font-family (invalidate-coll-self-reference token-name v)
|
||||||
|
(invalidate-self-reference token-name v))]
|
||||||
|
(assoc err :typography-key k)))
|
||||||
|
token-values)))
|
||||||
|
|
||||||
|
(defn validate-typography-token
|
||||||
|
[props]
|
||||||
|
(-> props
|
||||||
|
(update :token-value
|
||||||
|
(fn [v]
|
||||||
|
(-> (or v {})
|
||||||
|
(d/update-when :font-family #(if (string? %) (ctt/split-font-family %) %)))))
|
||||||
|
(assoc :validators [invalidate-typography-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.
|
||||||
Uses a custom debouncing logic, as the resolve function is async."
|
Uses a custom debouncing logic, as the resolve function is async."
|
||||||
[name-ref token tokens callback & {:keys [timeout] :or {timeout 160}}]
|
[{:keys [timeout name-ref token tokens callback validate-token]
|
||||||
|
:or {timeout 160}}]
|
||||||
(let [timeout-id-ref (mf/use-ref nil)
|
(let [timeout-id-ref (mf/use-ref nil)
|
||||||
debounced-resolver-callback
|
debounced-resolver-callback
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -160,14 +235,12 @@
|
|||||||
(js/setTimeout
|
(js/setTimeout
|
||||||
(fn []
|
(fn []
|
||||||
(when (not (timeout-outdated-cb?))
|
(when (not (timeout-outdated-cb?))
|
||||||
(->> (validate-token-value {:value value
|
(->> (validate-token {:token-value value
|
||||||
:name-value @name-ref
|
:token-name @name-ref
|
||||||
:token token
|
:prev-token token
|
||||||
:tokens tokens})
|
:tokens tokens})
|
||||||
(rx/filter #(not (timeout-outdated-cb?)))
|
(rx/filter #(not (timeout-outdated-cb?)))
|
||||||
(rx/subs!
|
(rx/subs! callback callback))))
|
||||||
callback
|
|
||||||
callback))))
|
|
||||||
timeout))))]
|
timeout))))]
|
||||||
debounced-resolver-callback))
|
debounced-resolver-callback))
|
||||||
|
|
||||||
@ -175,30 +248,35 @@
|
|||||||
|
|
||||||
;; Component -------------------------------------------------------------------
|
;; Component -------------------------------------------------------------------
|
||||||
|
|
||||||
(mf/defc token-value-hint
|
|
||||||
[{:keys [result]}]
|
|
||||||
(let [{:keys [errors warnings resolved-value]} result
|
|
||||||
empty-message? (nil? result)
|
|
||||||
|
|
||||||
message (cond
|
|
||||||
empty-message? (tr "workspace.tokens.resolved-value" "-")
|
|
||||||
warnings (wtw/humanize-warnings warnings)
|
|
||||||
errors (->> (wte/humanize-errors errors)
|
|
||||||
(str/join "\n"))
|
|
||||||
:else (tr "workspace.tokens.resolved-value" (or resolved-value result)))
|
|
||||||
type (cond
|
|
||||||
empty-message? "hint"
|
|
||||||
errors "error"
|
|
||||||
warnings "warning"
|
|
||||||
:else "hint")]
|
|
||||||
[:> hint-message*
|
|
||||||
{:id "token-value-hint"
|
|
||||||
:message message
|
|
||||||
:class (stl/css-case :resolved-value (not (or empty-message? (seq warnings) (seq errors))))
|
|
||||||
:type type}]))
|
|
||||||
|
|
||||||
(mf/defc form*
|
(mf/defc form*
|
||||||
[{:keys [token token-type action selected-token-set-name transform-value on-value-resolve custom-input-token-value custom-input-token-value-props input-value-placeholder]}]
|
"Form component to edit or create a token of any token type.
|
||||||
|
|
||||||
|
Callback props:
|
||||||
|
validate-token: Function to validate and resolve an editing token, see `default-validate-token`.
|
||||||
|
on-value-resolve: Will be called when a token value is resolved
|
||||||
|
Used to sync external state (like color picker)
|
||||||
|
on-get-token-value: Custom function to get the input value from the dom
|
||||||
|
(As there might be multiple inputs passed for `custom-input-token-value`)
|
||||||
|
Can also be used to manipulate the value (E.g.: Auto-prepending # for hex colors)
|
||||||
|
|
||||||
|
Custom component props:
|
||||||
|
custom-input-token-value: Custom component for editing/displaying the token value
|
||||||
|
custom-input-token-value-props: Custom props passed to the custom-input-token-value merged with the default props"
|
||||||
|
[{:keys [token
|
||||||
|
token-type
|
||||||
|
selected-token-set-name
|
||||||
|
action
|
||||||
|
input-value-placeholder
|
||||||
|
|
||||||
|
;; Callbacks
|
||||||
|
validate-token
|
||||||
|
on-value-resolve
|
||||||
|
on-get-token-value
|
||||||
|
|
||||||
|
;; Custom component props
|
||||||
|
custom-input-token-value
|
||||||
|
custom-input-token-value-props]
|
||||||
|
:or {validate-token default-validate-token}}]
|
||||||
(let [create? (not (instance? ctob/Token token))
|
(let [create? (not (instance? ctob/Token token))
|
||||||
token (or token {:type token-type})
|
token (or token {:type token-type})
|
||||||
token-properties (dwta/get-token-properties token)
|
token-properties (dwta/get-token-properties token)
|
||||||
@ -213,7 +291,10 @@
|
|||||||
|
|
||||||
;; Style dictionary resolver needs font families to be an array of strings
|
;; Style dictionary resolver needs font families to be an array of strings
|
||||||
(= :font-family (or (:type token) token-type))
|
(= :font-family (or (:type token) token-type))
|
||||||
(update-in [(:name token) :value] ctt/split-font-family))
|
(update-in [(:name token) :value] ctt/split-font-family)
|
||||||
|
|
||||||
|
(= :typography (or (:type token) token-type))
|
||||||
|
(d/update-in-when [(:name token) :font-family :value] ctt/split-font-family))
|
||||||
|
|
||||||
resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:cache-atom form-token-cache-atom
|
resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:cache-atom form-token-cache-atom
|
||||||
:interactive? true})
|
:interactive? true})
|
||||||
@ -288,37 +369,32 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-value-resolve)
|
(mf/deps on-value-resolve)
|
||||||
(fn [token-or-err]
|
(fn [token-or-err]
|
||||||
(let [error? (:errors token-or-err)
|
(when on-value-resolve
|
||||||
warnings? (:warnings token-or-err)
|
(cond
|
||||||
v (cond
|
(:errors token-or-err) (on-value-resolve nil)
|
||||||
error?
|
:else (on-value-resolve (:resolved-value token-or-err))))
|
||||||
(do
|
(reset! token-resolve-result* token-or-err)))
|
||||||
(when on-value-resolve (on-value-resolve nil))
|
|
||||||
token-or-err)
|
|
||||||
|
|
||||||
warnings?
|
on-update-value-debounced
|
||||||
(:warnings {:warnings token-or-err})
|
(use-debonced-resolve-callback
|
||||||
|
{:name-ref token-name-ref
|
||||||
|
:token token
|
||||||
|
:tokens active-theme-tokens
|
||||||
|
:callback set-resolve-value
|
||||||
|
:validate-token validate-token})
|
||||||
|
|
||||||
:else
|
;; Callback to update the value state via on of the inputs :on-change
|
||||||
(cond-> (:resolved-value token-or-err)
|
|
||||||
on-value-resolve on-value-resolve))]
|
|
||||||
(reset! token-resolve-result* v))))
|
|
||||||
|
|
||||||
on-update-value-debounced (use-debonced-resolve-callback token-name-ref token active-theme-tokens set-resolve-value)
|
|
||||||
on-update-value
|
on-update-value
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-update-value-debounced transform-value)
|
(mf/deps on-update-value-debounced on-get-token-value)
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(let [value (dom/get-target-val e)
|
(let [value (if (fn? on-get-token-value)
|
||||||
value' (if (fn? transform-value)
|
(on-get-token-value e (mf/ref-val value-ref))
|
||||||
(transform-value value)
|
(dom/get-target-val e))]
|
||||||
value)]
|
|
||||||
;; Value got updated in transform, update the dom node
|
|
||||||
(when (not= value value')
|
|
||||||
(dom/set-value! (mf/ref-val value-input-ref) value'))
|
|
||||||
(mf/set-ref-val! value-ref value)
|
(mf/set-ref-val! value-ref value)
|
||||||
(on-update-value-debounced value))))
|
(on-update-value-debounced value))))
|
||||||
|
|
||||||
|
;; Calback to update the value state from the outside (e.g.: color picker)
|
||||||
on-external-update-value
|
on-external-update-value
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-update-value-debounced)
|
(mf/deps on-update-value-debounced)
|
||||||
@ -328,10 +404,7 @@
|
|||||||
(on-update-value-debounced next-value)))
|
(on-update-value-debounced next-value)))
|
||||||
|
|
||||||
value-error? (seq (:errors token-resolve-result))
|
value-error? (seq (:errors token-resolve-result))
|
||||||
|
valid-value-field? (not value-error?)
|
||||||
valid-value-field? (and
|
|
||||||
(not value-error?)
|
|
||||||
(valid-value? token-resolve-result))
|
|
||||||
|
|
||||||
;; Description
|
;; Description
|
||||||
description-ref (mf/use-var (:description token))
|
description-ref (mf/use-var (:description token))
|
||||||
@ -359,7 +432,7 @@
|
|||||||
|
|
||||||
on-submit
|
on-submit
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps validate-name validate-descripion token active-theme-tokens)
|
(mf/deps create? validate-name validate-descripion token active-theme-tokens validate-token)
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(dom/prevent-default e)
|
(dom/prevent-default e)
|
||||||
;; We have to re-validate the current form values before submitting
|
;; We have to re-validate the current form values before submitting
|
||||||
@ -370,11 +443,7 @@
|
|||||||
valid-name? (try
|
valid-name? (try
|
||||||
(not (:errors (validate-name final-name)))
|
(not (:errors (validate-name final-name)))
|
||||||
(catch js/Error _ nil))
|
(catch js/Error _ nil))
|
||||||
final-value (let [value (mf/ref-val value-ref)
|
value (mf/ref-val value-ref)
|
||||||
font-family? (= :font-family (or (:type token) token-type))]
|
|
||||||
(if font-family?
|
|
||||||
(ctt/split-font-family value)
|
|
||||||
(finalize-value value)))
|
|
||||||
final-description @description-ref
|
final-description @description-ref
|
||||||
valid-description? (if final-description
|
valid-description? (if final-description
|
||||||
(try
|
(try
|
||||||
@ -382,22 +451,23 @@
|
|||||||
(catch js/Error _ nil))
|
(catch js/Error _ nil))
|
||||||
true)]
|
true)]
|
||||||
(when (and valid-name? valid-description?)
|
(when (and valid-name? valid-description?)
|
||||||
(->> (validate-token-value {:value final-value
|
(->> (validate-token {:token-value value
|
||||||
:name-value final-name
|
:token-name final-name
|
||||||
:token token
|
:token-description final-description
|
||||||
:tokens active-theme-tokens})
|
:prev-token token
|
||||||
|
:tokens active-theme-tokens})
|
||||||
(rx/subs!
|
(rx/subs!
|
||||||
(fn []
|
(fn [valid-token]
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(if (ctob/token? token)
|
(if create?
|
||||||
(dwtl/update-token (:id token)
|
|
||||||
{:name final-name
|
|
||||||
:value final-value
|
|
||||||
:description final-description})
|
|
||||||
|
|
||||||
(dwtl/create-token {:name final-name
|
(dwtl/create-token {:name final-name
|
||||||
:type token-type
|
:type token-type
|
||||||
:value final-value
|
:value (:value valid-token)
|
||||||
|
:description final-description})
|
||||||
|
|
||||||
|
(dwtl/update-token (:id token)
|
||||||
|
{:name final-name
|
||||||
|
:value (:value valid-token)
|
||||||
:description final-description}))
|
:description final-description}))
|
||||||
(dwtp/propagate-workspace-tokens)
|
(dwtp/propagate-workspace-tokens)
|
||||||
(modal/hide)))))))))
|
(modal/hide)))))))))
|
||||||
@ -496,29 +566,25 @@
|
|||||||
(let [placeholder (or input-value-placeholder (tr "workspace.tokens.token-value-enter"))
|
(let [placeholder (or input-value-placeholder (tr "workspace.tokens.token-value-enter"))
|
||||||
label (tr "workspace.tokens.token-value")
|
label (tr "workspace.tokens.token-value")
|
||||||
default-value (mf/ref-val value-ref)
|
default-value (mf/ref-val value-ref)
|
||||||
ref value-input-ref
|
ref value-input-ref]
|
||||||
error (not (nil? (:errors token-resolve-result)))
|
|
||||||
on-blur on-update-value]
|
|
||||||
(if (fn? custom-input-token-value)
|
(if (fn? custom-input-token-value)
|
||||||
[:> custom-input-token-value
|
[:> custom-input-token-value
|
||||||
{:placeholder placeholder
|
{:placeholder placeholder
|
||||||
:label label
|
:label label
|
||||||
:default-value default-value
|
:default-value default-value
|
||||||
:input-ref ref
|
:input-ref ref
|
||||||
:error error
|
|
||||||
:on-blur on-blur
|
|
||||||
:on-update-value on-update-value
|
:on-update-value on-update-value
|
||||||
:on-external-update-value on-external-update-value
|
:on-external-update-value on-external-update-value
|
||||||
:custom-input-token-value-props custom-input-token-value-props}]
|
:custom-input-token-value-props custom-input-token-value-props
|
||||||
|
:token-resolve-result token-resolve-result}]
|
||||||
[:> input-tokens-value*
|
[:> input-tokens-value*
|
||||||
{:placeholder placeholder
|
{:placeholder placeholder
|
||||||
:label label
|
:label label
|
||||||
:default-value default-value
|
:default-value default-value
|
||||||
:ref ref
|
:ref ref
|
||||||
:error error
|
:on-blur on-update-value
|
||||||
:on-blur on-blur
|
:on-change on-update-value
|
||||||
:on-change on-update-value}]))
|
:token-resolve-result token-resolve-result}]))]
|
||||||
[:& token-value-hint {:result token-resolve-result}]]
|
|
||||||
[:div {:class (stl/css :input-row)}
|
[:div {:class (stl/css :input-row)}
|
||||||
[:> input* {:label (tr "workspace.tokens.token-description")
|
[:> input* {:label (tr "workspace.tokens.token-description")
|
||||||
:placeholder (tr "workspace.tokens.token-description")
|
:placeholder (tr "workspace.tokens.token-description")
|
||||||
@ -606,7 +672,7 @@
|
|||||||
:on-change on-change'}]]))
|
:on-change on-change'}]]))
|
||||||
|
|
||||||
(mf/defc color-picker*
|
(mf/defc color-picker*
|
||||||
[{:keys [placeholder label default-value input-ref error on-blur on-update-value on-external-update-value custom-input-token-value-props]}]
|
[{:keys [placeholder label default-value input-ref on-blur on-update-value on-external-update-value custom-input-token-value-props token-resolve-result]}]
|
||||||
(let [{:keys [color on-display-colorpicker]} custom-input-token-value-props
|
(let [{:keys [color on-display-colorpicker]} custom-input-token-value-props
|
||||||
color-ramp-open* (mf/use-state false)
|
color-ramp-open* (mf/use-state false)
|
||||||
color-ramp-open? (deref color-ramp-open*)
|
color-ramp-open? (deref color-ramp-open*)
|
||||||
@ -658,14 +724,14 @@
|
|||||||
:label label
|
:label label
|
||||||
:default-value default-value
|
:default-value default-value
|
||||||
:ref input-ref
|
:ref input-ref
|
||||||
:error error
|
|
||||||
:on-blur on-blur
|
:on-blur on-blur
|
||||||
:on-change on-update-value
|
:on-change on-update-value
|
||||||
:slot-start swatch}]
|
:slot-start swatch}]
|
||||||
(when color-ramp-open?
|
(when color-ramp-open?
|
||||||
[:> ramp*
|
[:> ramp*
|
||||||
{:color (some-> color (tinycolor/valid-color))
|
{:color (some-> color (tinycolor/valid-color))
|
||||||
:on-change on-change'}])]))
|
:on-change on-change'}])
|
||||||
|
[:> token-value-hint* {:result token-resolve-result}]]))
|
||||||
|
|
||||||
(mf/defc color-form*
|
(mf/defc color-form*
|
||||||
[{:keys [token on-display-colorpicker] :rest props}]
|
[{:keys [token on-display-colorpicker] :rest props}]
|
||||||
@ -684,15 +750,19 @@
|
|||||||
{:color color
|
{:color color
|
||||||
:on-display-colorpicker on-display-colorpicker}))
|
:on-display-colorpicker on-display-colorpicker}))
|
||||||
|
|
||||||
transform-value
|
on-get-token-value
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [value]
|
(fn [e]
|
||||||
(if (tinycolor/hex-without-hash-prefix? value)
|
(let [value (dom/get-target-val e)]
|
||||||
(dm/str "#" value)
|
(if (tinycolor/hex-without-hash-prefix? value)
|
||||||
value)))]
|
(let [hex-value (dm/str "#" value)]
|
||||||
|
(dom/set-value! (dom/get-target e) hex-value)
|
||||||
|
hex-value)
|
||||||
|
value))))]
|
||||||
|
|
||||||
[:> form*
|
[:> form*
|
||||||
(mf/spread-props props {:token token
|
(mf/spread-props props {:token token
|
||||||
:transform-value transform-value
|
:on-get-token-value on-get-token-value
|
||||||
:on-value-resolve on-value-resolve
|
:on-value-resolve on-value-resolve
|
||||||
: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})]))
|
||||||
@ -713,7 +783,7 @@
|
|||||||
:full-size true}]]))
|
:full-size true}]]))
|
||||||
|
|
||||||
(mf/defc font-picker*
|
(mf/defc font-picker*
|
||||||
[{:keys [default-value input-ref error on-blur on-update-value on-external-update-value]}]
|
[{:keys [default-value input-ref on-blur on-update-value on-external-update-value token-resolve-result]}]
|
||||||
(let [font* (mf/use-state (fonts/find-font-family default-value))
|
(let [font* (mf/use-state (fonts/find-font-family default-value))
|
||||||
font (deref font*)
|
font (deref font*)
|
||||||
set-font (mf/use-fn
|
set-font (mf/use-fn
|
||||||
@ -764,11 +834,11 @@
|
|||||||
:label (tr "workspace.tokens.token-font-family-value")
|
:label (tr "workspace.tokens.token-font-family-value")
|
||||||
:default-value default-value
|
:default-value default-value
|
||||||
:ref input-ref
|
:ref input-ref
|
||||||
:error error
|
|
||||||
:on-blur on-blur
|
:on-blur on-blur
|
||||||
:on-change on-update-value'
|
:on-change on-update-value'
|
||||||
:icon "text-font-family"
|
:icon "text-font-family"
|
||||||
:slot-end font-selector-button}]
|
:slot-end font-selector-button
|
||||||
|
:token-resolve-result token-resolve-result}]
|
||||||
(when font-selector-open?
|
(when font-selector-open?
|
||||||
[:> font-selector-wrapper* {:font font
|
[:> font-selector-wrapper* {:font font
|
||||||
:input-ref input-ref
|
:input-ref input-ref
|
||||||
@ -785,7 +855,8 @@
|
|||||||
[:> form*
|
[:> form*
|
||||||
(mf/spread-props props {:token (when token (update token :value ctt/join-font-family))
|
(mf/spread-props props {:token (when token (update token :value ctt/join-font-family))
|
||||||
:custom-input-token-value font-picker*
|
:custom-input-token-value font-picker*
|
||||||
:on-value-resolve on-value-resolve})]))
|
:on-value-resolve on-value-resolve
|
||||||
|
:validate-token validate-font-family-token})]))
|
||||||
|
|
||||||
(mf/defc text-case-form*
|
(mf/defc text-case-form*
|
||||||
[{:keys [token] :rest props}]
|
[{:keys [token] :rest props}]
|
||||||
@ -805,11 +876,102 @@
|
|||||||
(mf/spread-props props {:token token
|
(mf/spread-props props {:token token
|
||||||
:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})])
|
:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})])
|
||||||
|
|
||||||
|
(def ^:private typography-inputs
|
||||||
|
#(d/ordered-map
|
||||||
|
:font-size
|
||||||
|
{:label "Font Size"
|
||||||
|
:placeholder (tr "workspace.tokens.token-value-enter")}
|
||||||
|
:font-family
|
||||||
|
{:label (tr "workspace.tokens.token-font-family-value")
|
||||||
|
:placeholder (tr "workspace.tokens.token-font-family-value-enter")}
|
||||||
|
:font-weight
|
||||||
|
{:label "Font Weight"
|
||||||
|
:placeholder (tr "workspace.tokens.font-weight-value-enter")}
|
||||||
|
:letter-spacing
|
||||||
|
{:label "Letter Spacing"
|
||||||
|
:placeholder (tr "workspace.tokens.token-value-enter")}
|
||||||
|
:text-case
|
||||||
|
{:label "Text Case"
|
||||||
|
:placeholder (tr "workspace.tokens.text-case-value-enter")}
|
||||||
|
:text-decoration
|
||||||
|
{:label "Text Decoration"
|
||||||
|
:placeholder (tr "workspace.tokens.text-decoration-value-enter")}))
|
||||||
|
|
||||||
|
(mf/defc typography-inputs*
|
||||||
|
[{:keys [default-value on-blur on-update-value token-resolve-result]}]
|
||||||
|
(let [typography-inputs (mf/use-memo typography-inputs)
|
||||||
|
errors-by-key (sd/collect-typography-errors token-resolve-result)]
|
||||||
|
[:div {:class (stl/css :nested-input-row)}
|
||||||
|
(for [[k {:keys [label placeholder]}] typography-inputs]
|
||||||
|
(let [value (get default-value k)
|
||||||
|
token-resolve-result
|
||||||
|
(-> {:resolved-value (let [v (get-in token-resolve-result [:resolved-value k])]
|
||||||
|
(when-not (str/empty? v) v))
|
||||||
|
:errors (get errors-by-key k)}
|
||||||
|
(d/without-nils))
|
||||||
|
|
||||||
|
input-ref (mf/use-ref)
|
||||||
|
|
||||||
|
on-external-update-value
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-update-value)
|
||||||
|
(fn [next-value]
|
||||||
|
(let [el (mf/ref-val input-ref)]
|
||||||
|
(dom/set-value! el next-value)
|
||||||
|
(on-update-value #js {:target el
|
||||||
|
:tokenType :font-family}))))
|
||||||
|
|
||||||
|
on-change
|
||||||
|
(mf/use-fn
|
||||||
|
;; Passing token-type via event to prevent deep function adapting & passing of type
|
||||||
|
(fn [e]
|
||||||
|
(-> (obj/set! e "tokenType" k)
|
||||||
|
(on-update-value))))]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :input-row)}
|
||||||
|
(case k
|
||||||
|
:font-family
|
||||||
|
[:> font-picker*
|
||||||
|
{:key (str k)
|
||||||
|
:label label
|
||||||
|
:placeholder placeholder
|
||||||
|
:input-ref input-ref
|
||||||
|
:default-value (when value (ctt/join-font-family value))
|
||||||
|
:on-blur on-blur
|
||||||
|
:on-update-value on-change
|
||||||
|
:on-external-update-value on-external-update-value
|
||||||
|
:token-resolve-result (when (seq token-resolve-result) token-resolve-result)}]
|
||||||
|
[:> input-tokens-value*
|
||||||
|
{:key (str k)
|
||||||
|
:label label
|
||||||
|
:placeholder placeholder
|
||||||
|
:default-value value
|
||||||
|
:on-blur on-blur
|
||||||
|
:on-change on-change
|
||||||
|
:token-resolve-result (when (seq token-resolve-result) token-resolve-result)}])]))]))
|
||||||
|
|
||||||
|
(mf/defc typography-form*
|
||||||
|
[{:keys [token] :rest props}]
|
||||||
|
(let [on-get-token-value
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [e prev-value]
|
||||||
|
(let [token-type (obj/get e "tokenType")
|
||||||
|
input-value (dom/get-target-val e)]
|
||||||
|
(if (empty? input-value)
|
||||||
|
(dissoc prev-value token-type)
|
||||||
|
(assoc prev-value token-type input-value)))))]
|
||||||
|
[:> form*
|
||||||
|
(mf/spread-props props {:token token
|
||||||
|
:custom-input-token-value typography-inputs*
|
||||||
|
:validate-token validate-typography-token
|
||||||
|
:on-get-token-value on-get-token-value})]))
|
||||||
|
|
||||||
(mf/defc form-wrapper*
|
(mf/defc form-wrapper*
|
||||||
[{:keys [token token-type] :as props}]
|
[{:keys [token token-type] :as props}]
|
||||||
(let [token-type' (or (:type token) token-type)]
|
(let [token-type' (or (:type token) token-type)]
|
||||||
(case token-type'
|
(case token-type'
|
||||||
:color [:> color-form* props]
|
:color [:> color-form* props]
|
||||||
|
:typography [:> typography-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]
|
||||||
|
|||||||
@ -39,6 +39,12 @@
|
|||||||
gap: $s-4;
|
gap: $s-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nested-input-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $s-12;
|
||||||
|
}
|
||||||
|
|
||||||
.warning-name-change-notification-wrapper {
|
.warning-name-change-notification-wrapper {
|
||||||
margin-block-start: $s-16;
|
margin-block-start: $s-16;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,14 @@
|
|||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.main.data.workspace.tokens.errors :as wte]
|
||||||
|
[app.main.data.workspace.tokens.warnings :as wtw]
|
||||||
|
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
|
||||||
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
||||||
[app.main.ui.ds.controls.utilities.label :refer [label*]]
|
[app.main.ui.ds.controls.utilities.label :refer [label*]]
|
||||||
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
|
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
|
[cuerdas.core :as str]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(def ^:private schema::input-tokens-value
|
(def ^:private schema::input-tokens-value
|
||||||
@ -22,14 +27,39 @@
|
|||||||
[:error {:optional true} :boolean]
|
[:error {:optional true} :boolean]
|
||||||
[:slot-start {:optional true} [:maybe some?]]
|
[:slot-start {:optional true} [:maybe some?]]
|
||||||
[:icon {:optional true}
|
[:icon {:optional true}
|
||||||
[:maybe [:and :string [:fn #(contains? icon-list %)]]]]])
|
[:maybe [:and :string [:fn #(contains? icon-list %)]]]]
|
||||||
|
[:token-resolve-result {:optional true} :any]])
|
||||||
|
|
||||||
|
(mf/defc token-value-hint*
|
||||||
|
[{:keys [result]}]
|
||||||
|
(let [{:keys [errors warnings resolved-value]} result
|
||||||
|
empty-message? (nil? result)
|
||||||
|
|
||||||
|
message (cond
|
||||||
|
empty-message? (tr "workspace.tokens.resolved-value" "-")
|
||||||
|
warnings (->> (wtw/humanize-warnings warnings)
|
||||||
|
(str/join "\n"))
|
||||||
|
errors (->> (wte/humanize-errors errors)
|
||||||
|
(str/join "\n"))
|
||||||
|
:else (tr "workspace.tokens.resolved-value" (or resolved-value result)))
|
||||||
|
type (cond
|
||||||
|
empty-message? "hint"
|
||||||
|
errors "error"
|
||||||
|
warnings "warning"
|
||||||
|
:else "hint")]
|
||||||
|
[:> hint-message*
|
||||||
|
{:id "token-value-hint"
|
||||||
|
:message message
|
||||||
|
:class (stl/css-case :resolved-value (not (or empty-message? (seq warnings) (seq errors))))
|
||||||
|
:type type}]))
|
||||||
|
|
||||||
(mf/defc input-tokens-value*
|
(mf/defc input-tokens-value*
|
||||||
{::mf/props :obj
|
{::mf/props :obj
|
||||||
::mf/forward-ref true
|
::mf/forward-ref true
|
||||||
::mf/schema schema::input-tokens-value}
|
::mf/schema schema::input-tokens-value}
|
||||||
[{:keys [class label placeholder error value icon slot-start] :rest props} ref]
|
[{:keys [class label placeholder value icon slot-start token-resolve-result] :rest props} ref]
|
||||||
(let [id (mf/use-id)
|
(let [error (not (nil? (:errors token-resolve-result)))
|
||||||
|
id (mf/use-id)
|
||||||
input-ref (mf/use-ref)
|
input-ref (mf/use-ref)
|
||||||
props (mf/spread-props props {:id id
|
props (mf/spread-props props {:id id
|
||||||
:type "text"
|
:type "text"
|
||||||
@ -41,7 +71,10 @@
|
|||||||
:slot-start slot-start
|
:slot-start slot-start
|
||||||
:icon icon
|
:icon icon
|
||||||
:ref (or ref input-ref)})]
|
:ref (or ref input-ref)})]
|
||||||
[:div {:class (dm/str class " " (stl/css-case :wrapper true
|
[:*
|
||||||
:input-error error))}
|
[:div {:class (dm/str class " " (stl/css-case :wrapper true
|
||||||
[:> label* {:for id} label]
|
:input-error error))}
|
||||||
[:> input-field* props]]))
|
[:> label* {:for id} label]
|
||||||
|
[:> input-field* props]]
|
||||||
|
(when token-resolve-result
|
||||||
|
[:> token-value-hint* {:result token-resolve-result}])]))
|
||||||
|
|||||||
@ -70,7 +70,7 @@
|
|||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[{:keys [x y position token token-type action selected-token-set-name] :as _args}]
|
[{:keys [x y position token token-type action selected-token-set-name] :as _args}]
|
||||||
(let [wrapper-style (use-viewport-position-style x y position (= token-type :color))
|
(let [wrapper-style (use-viewport-position-style x y position (= token-type :color))
|
||||||
modal-size-large* (mf/use-state false)
|
modal-size-large* (mf/use-state (= token-type :typography))
|
||||||
modal-size-large? (deref modal-size-large*)
|
modal-size-large? (deref modal-size-large*)
|
||||||
close-modal (mf/use-fn
|
close-modal (mf/use-fn
|
||||||
(fn []
|
(fn []
|
||||||
|
|||||||
@ -33,13 +33,13 @@
|
|||||||
:text-case "text-mixed"
|
:text-case "text-mixed"
|
||||||
:text-decoration "text-underlined"
|
:text-decoration "text-underlined"
|
||||||
:font-weight "text-font-weight"
|
:font-weight "text-font-weight"
|
||||||
|
:typography "text-typography"
|
||||||
:opacity "percentage"
|
:opacity "percentage"
|
||||||
:number "number"
|
:number "number"
|
||||||
:rotation "rotation"
|
:rotation "rotation"
|
||||||
:spacing "padding-extended"
|
:spacing "padding-extended"
|
||||||
:string "text-mixed"
|
:string "text-mixed"
|
||||||
:stroke-width "stroke-size"
|
:stroke-width "stroke-size"
|
||||||
:typography "text"
|
|
||||||
:dimensions "expand"
|
:dimensions "expand"
|
||||||
:sizing "expand"
|
:sizing "expand"
|
||||||
"add"))
|
"add"))
|
||||||
|
|||||||
@ -80,7 +80,13 @@
|
|||||||
:sizing "Sizing"
|
:sizing "Sizing"
|
||||||
:border-radius "Border Radius"
|
:border-radius "Border Radius"
|
||||||
:x "X"
|
:x "X"
|
||||||
:y "Y"})
|
:y "Y"
|
||||||
|
:font-size "Font Size"
|
||||||
|
:font-family "Font Family"
|
||||||
|
:font-weight "Font Weight"
|
||||||
|
:letter-spacing "Letter Spacing"
|
||||||
|
:text-case "Text Case"
|
||||||
|
:text-decoration "Text Decoration"})
|
||||||
|
|
||||||
;; Helper functions
|
;; Helper functions
|
||||||
|
|
||||||
@ -104,12 +110,23 @@
|
|||||||
(str/join ", " (map attribute-dictionary values)) ".")))
|
(str/join ", " (map attribute-dictionary values)) ".")))
|
||||||
grouped-values)))
|
grouped-values)))
|
||||||
|
|
||||||
|
(defn format-token-value [token-value]
|
||||||
|
(cond
|
||||||
|
(map? token-value)
|
||||||
|
(->> (map (fn [[k v]] (str "- " (category-dictionary k) ": " (format-token-value v))) token-value)
|
||||||
|
(str/join "\n")
|
||||||
|
(str "\n"))
|
||||||
|
|
||||||
|
(sequential? token-value)
|
||||||
|
(str/join "," token-value)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(str token-value)))
|
||||||
|
|
||||||
(defn- generate-tooltip
|
(defn- generate-tooltip
|
||||||
"Generates a tooltip for a given token"
|
"Generates a tooltip for a given token"
|
||||||
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set]
|
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set]
|
||||||
(let [{:keys [name type resolved-value]} token
|
(let [{:keys [name type resolved-value value]} token
|
||||||
value (cond->> (:value token)
|
|
||||||
(= :font-family type) ctt/join-font-family)
|
|
||||||
resolved-value-theme (:resolved-value theme-token)
|
resolved-value-theme (:resolved-value theme-token)
|
||||||
resolved-value (or resolved-value-theme resolved-value)
|
resolved-value (or resolved-value-theme resolved-value)
|
||||||
{:keys [title] :as token-props} (dwta/get-token-properties theme-token)
|
{:keys [title] :as token-props} (dwta/get-token-properties theme-token)
|
||||||
@ -125,8 +142,8 @@
|
|||||||
grouped-values (group-by dimensions-dictionary app-token-keys)
|
grouped-values (group-by dimensions-dictionary app-token-keys)
|
||||||
|
|
||||||
base-title (dm/str "Token: " name "\n"
|
base-title (dm/str "Token: " name "\n"
|
||||||
(tr "workspace.tokens.original-value" value) "\n"
|
(tr "workspace.tokens.original-value" (format-token-value value)) "\n"
|
||||||
(tr "workspace.tokens.resolved-value" resolved-value)
|
(tr "workspace.tokens.resolved-value" (format-token-value resolved-value))
|
||||||
(when (= (:type token) :number)
|
(when (= (:type token) :number)
|
||||||
(dm/str "\n" (tr "workspace.tokens.more-options"))))]
|
(dm/str "\n" (tr "workspace.tokens.more-options"))))]
|
||||||
|
|
||||||
|
|||||||
@ -860,6 +860,173 @@
|
|||||||
(t/is (nil? (:fill-color-ref-id fill)))
|
(t/is (nil? (:fill-color-ref-id fill)))
|
||||||
(t/is (nil? (:fill-color-ref-file fill))))))))))
|
(t/is (nil? (:fill-color-ref-file fill))))))))))
|
||||||
|
|
||||||
|
(t/deftest test-apply-typography-token
|
||||||
|
(t/testing "applies typography (composite) tokens"
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(let [font-size-token {:name "font-size-reference"
|
||||||
|
:value "100px"
|
||||||
|
:type :font-size}
|
||||||
|
font-family-token {:name "font-family-reference"
|
||||||
|
:value ["Arial" "sans-serif"]
|
||||||
|
:type :font-family}
|
||||||
|
typography-token {:name "typography.heading"
|
||||||
|
:value {:font-size "24px"
|
||||||
|
:font-weight "bold"
|
||||||
|
:font-family [(:font-id txt/default-text-attrs) "Arial" "sans-serif"]
|
||||||
|
:letter-spacing "2"
|
||||||
|
:text-case "uppercase"
|
||||||
|
:text-decoration "underline"}
|
||||||
|
:type :typography}
|
||||||
|
file (-> (setup-file-with-tokens)
|
||||||
|
(update-in [:data :tokens-lib]
|
||||||
|
#(-> %
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token font-size-token))
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token font-family-token))
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token typography-token)))))
|
||||||
|
store (ths/setup-store file)
|
||||||
|
text-1 (cths/get-shape file :text-1)
|
||||||
|
events [(dwta/apply-token {:shape-ids [(:id text-1)]
|
||||||
|
:attributes #{:typography}
|
||||||
|
:token (toht/get-token file "typography.heading")
|
||||||
|
:on-update-shape dwta/update-typography})]]
|
||||||
|
(tohs/run-store-async
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-state new-state)
|
||||||
|
text-1' (cths/get-shape file' :text-1)
|
||||||
|
text-1' (def text-1' text-1')
|
||||||
|
style-text-blocks (->> (:content text-1')
|
||||||
|
(txt/content->text+styles)
|
||||||
|
(remove (fn [[_ text]] (str/empty? (str/trim text))))
|
||||||
|
(mapv (fn [[style text]]
|
||||||
|
{:styles (merge txt/default-text-attrs style)
|
||||||
|
:text-content text}))
|
||||||
|
(first)
|
||||||
|
(:styles))]
|
||||||
|
(t/is (some? (:applied-tokens text-1')))
|
||||||
|
(t/is (= (:typography (:applied-tokens text-1')) "typography.heading"))
|
||||||
|
|
||||||
|
(t/is (= (:font-size style-text-blocks) "24"))
|
||||||
|
(t/is (= (:font-weight style-text-blocks) "400"))
|
||||||
|
(t/is (= (:font-family style-text-blocks) "sourcesanspro"))
|
||||||
|
(t/is (= (:letter-spacing style-text-blocks) "2"))
|
||||||
|
(t/is (= (:text-transform style-text-blocks) "uppercase"))
|
||||||
|
(t/is (= (:text-decoration style-text-blocks) "underline")))))))))
|
||||||
|
|
||||||
|
(t/deftest test-apply-reference-typography-token
|
||||||
|
(t/testing "applies typography (composite) tokens with references"
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(let [font-size-token {:name "fontSize"
|
||||||
|
:value "100px"
|
||||||
|
:type :font-size}
|
||||||
|
font-family-token {:name "fontFamily"
|
||||||
|
:value ["Arial" "sans-serif"]
|
||||||
|
:type :font-family}
|
||||||
|
typography-token {:name "typography"
|
||||||
|
:value {:font-size "{fontSize}"
|
||||||
|
:font-family ["{fontFamily}"]}
|
||||||
|
:type :typography}
|
||||||
|
file (-> (setup-file-with-tokens)
|
||||||
|
(update-in [:data :tokens-lib]
|
||||||
|
#(-> %
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token font-size-token))
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token font-family-token))
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token typography-token)))))
|
||||||
|
store (ths/setup-store file)
|
||||||
|
text-1 (cths/get-shape file :text-1)
|
||||||
|
events [(dwta/apply-token {:shape-ids [(:id text-1)]
|
||||||
|
:attributes #{:typography}
|
||||||
|
:token (toht/get-token file "typography")
|
||||||
|
:on-update-shape dwta/update-typography})]]
|
||||||
|
(tohs/run-store-async
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-state new-state)
|
||||||
|
text-1' (cths/get-shape file' :text-1)
|
||||||
|
style-text-blocks (->> (:content text-1')
|
||||||
|
(txt/content->text+styles)
|
||||||
|
(remove (fn [[_ text]] (str/empty? (str/trim text))))
|
||||||
|
(mapv (fn [[style text]]
|
||||||
|
{:styles (merge txt/default-text-attrs style)
|
||||||
|
:text-content text}))
|
||||||
|
(first)
|
||||||
|
(:styles))]
|
||||||
|
(t/is (some? (:applied-tokens text-1')))
|
||||||
|
(t/is (= (:typography (:applied-tokens text-1')) "typography"))
|
||||||
|
|
||||||
|
(t/is (= (:font-size style-text-blocks) "100"))
|
||||||
|
(t/is (= (:font-family style-text-blocks) "Arial")))))))))
|
||||||
|
|
||||||
|
(t/deftest test-unapply-atomic-tokens-on-composite-apply
|
||||||
|
(t/testing "unapplies atomic typography tokens when applying composite token"
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(let [font-size-token {:name "fontSize"
|
||||||
|
:value "100px"
|
||||||
|
:type :font-size}
|
||||||
|
typography-token {:name "typography"
|
||||||
|
:value {}
|
||||||
|
:type :typography}
|
||||||
|
file (-> (setup-file-with-tokens)
|
||||||
|
(update-in [:data :tokens-lib]
|
||||||
|
#(-> %
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token font-size-token))
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token typography-token)))))
|
||||||
|
store (ths/setup-store file)
|
||||||
|
text-1 (cths/get-shape file :text-1)
|
||||||
|
events [(dwta/apply-token {:shape-ids [(:id text-1)]
|
||||||
|
:attributes #{:typography}
|
||||||
|
:token (toht/get-token file "fontSize")})
|
||||||
|
(dwta/apply-token {:shape-ids [(:id text-1)]
|
||||||
|
:attributes #{:typography}
|
||||||
|
:token (toht/get-token file "typography")
|
||||||
|
:on-update-shape dwta/update-typography})]]
|
||||||
|
(tohs/run-store-async
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-state new-state)
|
||||||
|
text-1' (cths/get-shape file' :text-1)]
|
||||||
|
(t/is (some? (:applied-tokens text-1')))
|
||||||
|
(t/is (= (:typography (:applied-tokens text-1')) "typography"))
|
||||||
|
(t/is (nil? (:font-size (:applied-tokens text-1')))))))))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest test-unapply-composite-tokens-on-atomic-apply
|
||||||
|
(t/testing "unapplies composite typography tokens when applying atomic token"
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(let [font-size-token {:name "fontSize"
|
||||||
|
:value "100px"
|
||||||
|
:type :font-size}
|
||||||
|
typography-token {:name "typography"
|
||||||
|
:value {}
|
||||||
|
:type :typography}
|
||||||
|
file (-> (setup-file-with-tokens)
|
||||||
|
(update-in [:data :tokens-lib]
|
||||||
|
#(-> %
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token font-size-token))
|
||||||
|
(ctob/add-token-in-set "Set A" (ctob/make-token typography-token)))))
|
||||||
|
store (ths/setup-store file)
|
||||||
|
text-1 (cths/get-shape file :text-1)
|
||||||
|
events [(dwta/apply-token {:shape-ids [(:id text-1)]
|
||||||
|
:attributes #{:typography}
|
||||||
|
:token (toht/get-token file "typography")
|
||||||
|
:on-update-shape dwta/update-typography})
|
||||||
|
(dwta/apply-token {:shape-ids [(:id text-1)]
|
||||||
|
:attributes #{:font-size}
|
||||||
|
:token (toht/get-token file "fontSize")
|
||||||
|
:on-update-shape dwta/update-font-size})]]
|
||||||
|
(tohs/run-store-async
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-state new-state)
|
||||||
|
text-1' (cths/get-shape file' :text-1)]
|
||||||
|
(t/is (some? (:applied-tokens text-1')))
|
||||||
|
(t/is (= (:font-size (:applied-tokens text-1')) "fontSize"))
|
||||||
|
(t/is (nil? (:typography (:applied-tokens text-1')))))))))))
|
||||||
|
|
||||||
(t/deftest test-detach-styles-typography
|
(t/deftest test-detach-styles-typography
|
||||||
(t/testing "applying any typography token to a shape with a typography style should detach the style"
|
(t/testing "applying any typography token to a shape with a typography style should detach the style"
|
||||||
(t/async
|
(t/async
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user