mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
🎉 Add composite typography token
This commit is contained in:
committed by
Andrés Moya
parent
68a95cf0d0
commit
9106617436
@@ -48,7 +48,8 @@
|
||||
:stroke-width "borderWidth"
|
||||
:text-case "textCase"
|
||||
:text-decoration "textDecoration"
|
||||
:font-weight "fontWeights"})
|
||||
:font-weight "fontWeights"
|
||||
:typography "typography"})
|
||||
|
||||
(def dtcg-token-type->token-type
|
||||
(set/map-invert token-type->dtcg-token-type))
|
||||
@@ -180,6 +181,12 @@
|
||||
|
||||
(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
|
||||
[:map
|
||||
[:text-decoration {:optional true} token-name-ref]])
|
||||
@@ -192,7 +199,8 @@
|
||||
font-weight-keys
|
||||
text-case-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.
|
||||
;; Delete this once the typography feature flag is removed.
|
||||
@@ -215,6 +223,7 @@
|
||||
axis-keys
|
||||
rotation-keys
|
||||
typography-keys
|
||||
typography-token-keys
|
||||
number-keys))
|
||||
|
||||
(def ^:private schema:tokens
|
||||
@@ -260,12 +269,13 @@
|
||||
changed-sub-attr
|
||||
#{:m1 :m2 :m3 :m4})
|
||||
|
||||
(font-size-keys shape-attr) #{shape-attr}
|
||||
(letter-spacing-keys shape-attr) #{shape-attr}
|
||||
(font-family-keys shape-attr) #{shape-attr}
|
||||
(text-case-keys shape-attr) #{shape-attr}
|
||||
(text-decoration-keys shape-attr) #{shape-attr}
|
||||
(font-weight-keys shape-attr) #{shape-attr}
|
||||
(font-size-keys shape-attr) #{shape-attr :typography}
|
||||
(letter-spacing-keys shape-attr) #{shape-attr :typography}
|
||||
(font-family-keys shape-attr) #{shape-attr :typography}
|
||||
(text-case-keys shape-attr) #{shape-attr :typography}
|
||||
(text-decoration-keys shape-attr) #{shape-attr :typography}
|
||||
(font-weight-keys shape-attr) #{shape-attr :typography}
|
||||
|
||||
(border-radius-keys shape-attr) #{shape-attr}
|
||||
(sizing-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);
|
||||
if (flags.length) {
|
||||
await workspacePage.mockConfigFlags(flags);
|
||||
}
|
||||
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
"get-team?id=*",
|
||||
"workspace/get-team-tokens.json",
|
||||
);
|
||||
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-tokens.json");
|
||||
await workspacePage.mockRPC(
|
||||
/get\-file\-fragment\?/,
|
||||
"workspace/get-file-fragment-tokens.json",
|
||||
);
|
||||
await workspacePage.mockRPC(/get\-file\?/, file);
|
||||
await workspacePage.mockRPC(/get\-file\-fragment\?/, fileFragment);
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"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("Clicking tokens tab button opens tokens sidebar tab", async ({
|
||||
page,
|
||||
@@ -531,9 +547,7 @@ test.describe("Tokens: Sets Tab", () => {
|
||||
|
||||
const assertEmptySetsList = async (el) => {
|
||||
const buttons = await el.getByRole("button").allTextContents();
|
||||
const filteredButtons = buttons.filter(
|
||||
(text) => text === "Create one.",
|
||||
);
|
||||
const filteredButtons = buttons.filter((text) => text === "Create one.");
|
||||
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");
|
||||
});
|
||||
|
||||
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."
|
||||
(do
|
||||
(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"
|
||||
:format (fn [^js res]
|
||||
(.-tokens (.-dictionary res)))})
|
||||
(.. res -dictionary -tokens))})
|
||||
sd))
|
||||
|
||||
(def default-config
|
||||
{:platforms {:json
|
||||
{:transformGroup "tokens-studio"
|
||||
{:transformGroup "penpot"
|
||||
;; Required: The StyleDictionary API is focused on files even when working in the browser
|
||||
:files [{:format "custom/json" :destination "penpot"}]}}
|
||||
:preprocessors ["tokens-studio"]
|
||||
@@ -64,7 +71,6 @@
|
||||
(and (string? 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
|
||||
"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`."
|
||||
@@ -113,9 +119,9 @@
|
||||
"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 parseable but is out of range returns a map with `warnings`."
|
||||
[value has-references?]
|
||||
|
||||
(let [parsed-value (cft/parse-token-value value)
|
||||
[value]
|
||||
(let [missing-references? (seq (ctob/find-token-value-references value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||
references (seq (ctob/find-token-value-references value))]
|
||||
(cond (and parsed-value (not out-of-scope))
|
||||
@@ -125,10 +131,10 @@
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference 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)]}
|
||||
|
||||
(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)])
|
||||
|
||||
: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\"}`.
|
||||
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`."
|
||||
[value has-references?]
|
||||
(let [parsed-value (cft/parse-token-value 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)
|
||||
references (seq (ctob/find-token-value-references value))]
|
||||
(cond
|
||||
@@ -149,10 +156,10 @@
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference 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)]}
|
||||
|
||||
(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)])
|
||||
|
||||
:else {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]})))
|
||||
@@ -218,6 +225,55 @@
|
||||
:else
|
||||
{: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
|
||||
"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
|
||||
@@ -254,18 +310,15 @@
|
||||
(fn [acc ^js sd-token]
|
||||
(let [origin-token (get-origin-token sd-token)
|
||||
value (.-value sd-token)
|
||||
has-references? (str/includes? (:value origin-token) "{")
|
||||
parsed-token-value (case (:type origin-token)
|
||||
:font-family {:value (-> (js->clj value) (flatten))}
|
||||
parsed-token-value (or
|
||||
(parse-atomic-typography-value (:type origin-token) value)
|
||||
(case (:type origin-token)
|
||||
:typography (parse-composite-typography-value value)
|
||||
:color (parse-sd-token-color-value value)
|
||||
:opacity (parse-sd-token-opacity-value value has-references?)
|
||||
:stroke-width (parse-sd-token-stroke-width-value value has-references?)
|
||||
:text-case (parse-sd-token-text-case-value value)
|
||||
:letter-spacing (parse-sd-token-letter-spacing-value value)
|
||||
:text-decoration (parse-sd-token-text-decoration-value value)
|
||||
:font-weight (parse-sd-token-font-weight-value value)
|
||||
:opacity (parse-sd-token-opacity-value value)
|
||||
:stroke-width (parse-sd-token-stroke-width-value value)
|
||||
:number (parse-sd-token-number-value value)
|
||||
(parse-sd-token-general-value value))
|
||||
(parse-sd-token-general-value value)))
|
||||
output-token (cond (:errors parsed-token-value)
|
||||
(merge origin-token parsed-token-value)
|
||||
|
||||
|
||||
@@ -369,6 +369,33 @@
|
||||
:timeout 7000}))]
|
||||
(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 ------------------------------------------------------------
|
||||
|
||||
(defn apply-token
|
||||
@@ -383,6 +410,12 @@
|
||||
(watch [_ state _]
|
||||
;; We do not allow to apply tokens while text editor is open.
|
||||
(when (empty? (get state :workspace-editor-state))
|
||||
(let [attributes-to-remove
|
||||
;; Remove atomic typography tokens when applying composite and vice-verca
|
||||
(cond
|
||||
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
||||
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
||||
:else attributes-to-remove)]
|
||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)
|
||||
(ctob/get-tokens-in-active-sets))]
|
||||
@@ -405,6 +438,7 @@
|
||||
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
|
||||
tokenized-attributes (cft/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
(st/emit! (ev/event {::ev/name "apply-tokens"
|
||||
:type type
|
||||
@@ -415,10 +449,14 @@
|
||||
attributes-to-remove
|
||||
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||
:always
|
||||
(update :applied-tokens merge tokenized-attributes))))
|
||||
(update :applied-tokens merge tokenized-attributes)))))
|
||||
(when on-update-shape
|
||||
(on-update-shape resolved-value shape-ids attributes))
|
||||
(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
|
||||
"Handles edge-case for spacing token when applying token via toggle button.
|
||||
@@ -554,6 +592,14 @@
|
||||
:fields [{:label "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
|
||||
{:title "Text Decoration"
|
||||
:attributes ctt/text-decoration-keys
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#{:text-case} dwta/update-text-case
|
||||
#{:text-decoration} dwta/update-text-decoration
|
||||
#{:font-weight} dwta/update-font-weight
|
||||
#{:typography} dwta/update-typography
|
||||
#{:x :y} dwta/update-shape-position
|
||||
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding
|
||||
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin
|
||||
@@ -161,7 +162,10 @@
|
||||
(collect-shapes-update-info resolved-tokens (:objects page))
|
||||
|
||||
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"
|
||||
:hint "propagate-tokens"
|
||||
@@ -170,7 +174,9 @@
|
||||
::l/sync? true)
|
||||
|
||||
(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/mapcat (fn [frame-id]
|
||||
(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.library-edit :as dwtl]
|
||||
[app.main.data.workspace.tokens.propagation :as dwtp]
|
||||
[app.main.data.workspace.tokens.warnings :as wtw]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -36,11 +35,13 @@
|
||||
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-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-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.functions :as uf]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[app.util.object :as obj]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]
|
||||
@@ -101,53 +102,127 @@
|
||||
|
||||
;; Validation ------------------------------------------------------------------
|
||||
|
||||
(defn validate-empty-input [value]
|
||||
(if (sequential? value)
|
||||
(empty? value)
|
||||
(empty? (str/trim value))))
|
||||
(defn invalidate-empty-value [token-value]
|
||||
(when (empty? (str/trim token-value))
|
||||
(wte/get-error-code :error.token/empty-input)))
|
||||
|
||||
(defn validate-self-reference? [token-name value]
|
||||
(if (sequential? value)
|
||||
(some #(ctob/token-value-self-reference? token-name %) value)
|
||||
(ctob/token-value-self-reference? token-name value)))
|
||||
(defn invalidate-token-empty-value [token]
|
||||
(invalidate-empty-value (:value token)))
|
||||
|
||||
(defn validate-token-value
|
||||
"Validates token value by resolving the value `input` using `StyleDictionary`.
|
||||
Returns a promise of either resolved tokens or rejects with an error state."
|
||||
[{: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)]})
|
||||
(defn invalidate-self-reference [token-name token-value]
|
||||
(when (ctob/token-value-self-reference? token-name token-value)
|
||||
(wte/get-error-code :error.token/direct-self-reference)))
|
||||
|
||||
(validate-self-reference? token-name value)
|
||||
(rx/throw {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
|
||||
(defn invalidate-token-self-reference [token]
|
||||
(invalidate-self-reference (:name token) (:value token)))
|
||||
|
||||
:else
|
||||
(let [tokens' (cond-> tokens
|
||||
(defn validate-resolve-token
|
||||
[token prev-token tokens]
|
||||
(let [token (cond-> token
|
||||
;; When creating a new token we dont have a name yet,
|
||||
;; but we still want to resolve the value to show in the form.
|
||||
;; So we use a temporary token name that hopefully doesn't clash with any of the users token names
|
||||
(str/empty? (:name token)) (assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"))
|
||||
tokens' (cond-> tokens
|
||||
;; Remove previous token when renaming a token
|
||||
(not= name-value (:name token)) (dissoc (:name token))
|
||||
:always (update token-name #(ctob/make-token (merge % {:value (cond
|
||||
(= (:type token) :font-family) (ctt/split-font-family value)
|
||||
:else value)
|
||||
:name token-name
|
||||
:type (:type token)}))))]
|
||||
(not= (:name token) (:name prev-token))
|
||||
(dissoc (:name prev-token))
|
||||
|
||||
:always
|
||||
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||
(->> tokens'
|
||||
(sd/resolve-tokens-interactive)
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
|
||||
(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 errors (wte/get-error-code :error/unknown-error))}))))))))))
|
||||
: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
|
||||
"Resolves a token values using `StyleDictionary`.
|
||||
This function is debounced as the resolving might be an expensive calculation.
|
||||
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)
|
||||
debounced-resolver-callback
|
||||
(mf/use-fn
|
||||
@@ -160,14 +235,12 @@
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(when (not (timeout-outdated-cb?))
|
||||
(->> (validate-token-value {:value value
|
||||
:name-value @name-ref
|
||||
:token token
|
||||
(->> (validate-token {:token-value value
|
||||
:token-name @name-ref
|
||||
:prev-token token
|
||||
:tokens tokens})
|
||||
(rx/filter #(not (timeout-outdated-cb?)))
|
||||
(rx/subs!
|
||||
callback
|
||||
callback))))
|
||||
(rx/subs! callback callback))))
|
||||
timeout))))]
|
||||
debounced-resolver-callback))
|
||||
|
||||
@@ -175,30 +248,35 @@
|
||||
|
||||
;; 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*
|
||||
[{: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))
|
||||
token (or token {:type token-type})
|
||||
token-properties (dwta/get-token-properties token)
|
||||
@@ -213,7 +291,10 @@
|
||||
|
||||
;; Style dictionary resolver needs font families to be an array of strings
|
||||
(= :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
|
||||
:interactive? true})
|
||||
@@ -288,37 +369,32 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-value-resolve)
|
||||
(fn [token-or-err]
|
||||
(let [error? (:errors token-or-err)
|
||||
warnings? (:warnings token-or-err)
|
||||
v (cond
|
||||
error?
|
||||
(do
|
||||
(when on-value-resolve (on-value-resolve nil))
|
||||
token-or-err)
|
||||
(when on-value-resolve
|
||||
(cond
|
||||
(:errors token-or-err) (on-value-resolve nil)
|
||||
:else (on-value-resolve (:resolved-value token-or-err))))
|
||||
(reset! token-resolve-result* token-or-err)))
|
||||
|
||||
warnings?
|
||||
(:warnings {:warnings token-or-err})
|
||||
on-update-value-debounced
|
||||
(use-debonced-resolve-callback
|
||||
{:name-ref token-name-ref
|
||||
:token token
|
||||
:tokens active-theme-tokens
|
||||
:callback set-resolve-value
|
||||
:validate-token validate-token})
|
||||
|
||||
:else
|
||||
(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)
|
||||
;; Callback to update the value state via on of the inputs :on-change
|
||||
on-update-value
|
||||
(mf/use-fn
|
||||
(mf/deps on-update-value-debounced transform-value)
|
||||
(mf/deps on-update-value-debounced on-get-token-value)
|
||||
(fn [e]
|
||||
(let [value (dom/get-target-val e)
|
||||
value' (if (fn? transform-value)
|
||||
(transform-value value)
|
||||
value)]
|
||||
;; Value got updated in transform, update the dom node
|
||||
(when (not= value value')
|
||||
(dom/set-value! (mf/ref-val value-input-ref) value'))
|
||||
(let [value (if (fn? on-get-token-value)
|
||||
(on-get-token-value e (mf/ref-val value-ref))
|
||||
(dom/get-target-val e))]
|
||||
(mf/set-ref-val! value-ref value)
|
||||
(on-update-value-debounced value))))
|
||||
|
||||
;; Calback to update the value state from the outside (e.g.: color picker)
|
||||
on-external-update-value
|
||||
(mf/use-fn
|
||||
(mf/deps on-update-value-debounced)
|
||||
@@ -328,10 +404,7 @@
|
||||
(on-update-value-debounced next-value)))
|
||||
|
||||
value-error? (seq (:errors token-resolve-result))
|
||||
|
||||
valid-value-field? (and
|
||||
(not value-error?)
|
||||
(valid-value? token-resolve-result))
|
||||
valid-value-field? (not value-error?)
|
||||
|
||||
;; Description
|
||||
description-ref (mf/use-var (:description token))
|
||||
@@ -359,7 +432,7 @@
|
||||
|
||||
on-submit
|
||||
(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]
|
||||
(dom/prevent-default e)
|
||||
;; We have to re-validate the current form values before submitting
|
||||
@@ -370,11 +443,7 @@
|
||||
valid-name? (try
|
||||
(not (:errors (validate-name final-name)))
|
||||
(catch js/Error _ nil))
|
||||
final-value (let [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)))
|
||||
value (mf/ref-val value-ref)
|
||||
final-description @description-ref
|
||||
valid-description? (if final-description
|
||||
(try
|
||||
@@ -382,22 +451,23 @@
|
||||
(catch js/Error _ nil))
|
||||
true)]
|
||||
(when (and valid-name? valid-description?)
|
||||
(->> (validate-token-value {:value final-value
|
||||
:name-value final-name
|
||||
:token token
|
||||
(->> (validate-token {:token-value value
|
||||
:token-name final-name
|
||||
:token-description final-description
|
||||
:prev-token token
|
||||
:tokens active-theme-tokens})
|
||||
(rx/subs!
|
||||
(fn []
|
||||
(fn [valid-token]
|
||||
(st/emit!
|
||||
(if (ctob/token? token)
|
||||
(dwtl/update-token (:id token)
|
||||
{:name final-name
|
||||
:value final-value
|
||||
:description final-description})
|
||||
|
||||
(if create?
|
||||
(dwtl/create-token {:name final-name
|
||||
: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}))
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide)))))))))
|
||||
@@ -496,29 +566,25 @@
|
||||
(let [placeholder (or input-value-placeholder (tr "workspace.tokens.token-value-enter"))
|
||||
label (tr "workspace.tokens.token-value")
|
||||
default-value (mf/ref-val value-ref)
|
||||
ref value-input-ref
|
||||
error (not (nil? (:errors token-resolve-result)))
|
||||
on-blur on-update-value]
|
||||
ref value-input-ref]
|
||||
(if (fn? custom-input-token-value)
|
||||
[:> custom-input-token-value
|
||||
{:placeholder placeholder
|
||||
:label label
|
||||
:default-value default-value
|
||||
:input-ref ref
|
||||
:error error
|
||||
:on-blur on-blur
|
||||
:on-update-value on-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*
|
||||
{:placeholder placeholder
|
||||
:label label
|
||||
:default-value default-value
|
||||
:ref ref
|
||||
:error error
|
||||
:on-blur on-blur
|
||||
:on-change on-update-value}]))
|
||||
[:& token-value-hint {:result token-resolve-result}]]
|
||||
:on-blur on-update-value
|
||||
:on-change on-update-value
|
||||
:token-resolve-result token-resolve-result}]))]
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> input* {:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
@@ -606,7 +672,7 @@
|
||||
:on-change on-change'}]]))
|
||||
|
||||
(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
|
||||
color-ramp-open* (mf/use-state false)
|
||||
color-ramp-open? (deref color-ramp-open*)
|
||||
@@ -658,14 +724,14 @@
|
||||
:label label
|
||||
:default-value default-value
|
||||
:ref input-ref
|
||||
:error error
|
||||
:on-blur on-blur
|
||||
:on-change on-update-value
|
||||
:slot-start swatch}]
|
||||
(when color-ramp-open?
|
||||
[:> ramp*
|
||||
{: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*
|
||||
[{:keys [token on-display-colorpicker] :rest props}]
|
||||
@@ -684,15 +750,19 @@
|
||||
{:color color
|
||||
:on-display-colorpicker on-display-colorpicker}))
|
||||
|
||||
transform-value
|
||||
on-get-token-value
|
||||
(mf/use-fn
|
||||
(fn [value]
|
||||
(fn [e]
|
||||
(let [value (dom/get-target-val e)]
|
||||
(if (tinycolor/hex-without-hash-prefix? value)
|
||||
(dm/str "#" value)
|
||||
value)))]
|
||||
(let [hex-value (dm/str "#" value)]
|
||||
(dom/set-value! (dom/get-target e) hex-value)
|
||||
hex-value)
|
||||
value))))]
|
||||
|
||||
[:> form*
|
||||
(mf/spread-props props {:token token
|
||||
:transform-value transform-value
|
||||
:on-get-token-value on-get-token-value
|
||||
:on-value-resolve on-value-resolve
|
||||
:custom-input-token-value color-picker*
|
||||
:custom-input-token-value-props custom-input-token-value-props})]))
|
||||
@@ -713,7 +783,7 @@
|
||||
:full-size true}]]))
|
||||
|
||||
(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))
|
||||
font (deref font*)
|
||||
set-font (mf/use-fn
|
||||
@@ -764,11 +834,11 @@
|
||||
:label (tr "workspace.tokens.token-font-family-value")
|
||||
:default-value default-value
|
||||
:ref input-ref
|
||||
:error error
|
||||
:on-blur on-blur
|
||||
:on-change on-update-value'
|
||||
:icon "text-font-family"
|
||||
:slot-end font-selector-button}]
|
||||
:slot-end font-selector-button
|
||||
:token-resolve-result token-resolve-result}]
|
||||
(when font-selector-open?
|
||||
[:> font-selector-wrapper* {:font font
|
||||
:input-ref input-ref
|
||||
@@ -785,7 +855,8 @@
|
||||
[:> form*
|
||||
(mf/spread-props props {:token (when token (update token :value ctt/join-font-family))
|
||||
: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*
|
||||
[{:keys [token] :rest props}]
|
||||
@@ -805,11 +876,102 @@
|
||||
(mf/spread-props props {:token token
|
||||
: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*
|
||||
[{:keys [token token-type] :as props}]
|
||||
(let [token-type' (or (:type token) token-type)]
|
||||
(case token-type'
|
||||
:color [:> color-form* props]
|
||||
:typography [:> typography-form* props]
|
||||
:font-family [:> font-family-form* props]
|
||||
:text-case [:> text-case-form* props]
|
||||
:text-decoration [:> text-decoration-form* props]
|
||||
|
||||
@@ -39,6 +39,12 @@
|
||||
gap: $s-4;
|
||||
}
|
||||
|
||||
.nested-input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-12;
|
||||
}
|
||||
|
||||
.warning-name-change-notification-wrapper {
|
||||
margin-block-start: $s-16;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,14 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[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.label :refer [label*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema::input-tokens-value
|
||||
@@ -22,14 +27,39 @@
|
||||
[:error {:optional true} :boolean]
|
||||
[:slot-start {:optional true} [:maybe some?]]
|
||||
[: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/props :obj
|
||||
::mf/forward-ref true
|
||||
::mf/schema schema::input-tokens-value}
|
||||
[{:keys [class label placeholder error value icon slot-start] :rest props} ref]
|
||||
(let [id (mf/use-id)
|
||||
[{:keys [class label placeholder value icon slot-start token-resolve-result] :rest props} ref]
|
||||
(let [error (not (nil? (:errors token-resolve-result)))
|
||||
id (mf/use-id)
|
||||
input-ref (mf/use-ref)
|
||||
props (mf/spread-props props {:id id
|
||||
:type "text"
|
||||
@@ -41,7 +71,10 @@
|
||||
:slot-start slot-start
|
||||
:icon icon
|
||||
:ref (or ref input-ref)})]
|
||||
[:*
|
||||
[:div {:class (dm/str class " " (stl/css-case :wrapper true
|
||||
:input-error error))}
|
||||
[:> label* {:for id} label]
|
||||
[:> input-field* props]]))
|
||||
[:> input-field* props]]
|
||||
(when token-resolve-result
|
||||
[:> token-value-hint* {:result token-resolve-result}])]))
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{::mf/wrap-props false}
|
||||
[{: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))
|
||||
modal-size-large* (mf/use-state false)
|
||||
modal-size-large* (mf/use-state (= token-type :typography))
|
||||
modal-size-large? (deref modal-size-large*)
|
||||
close-modal (mf/use-fn
|
||||
(fn []
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
:text-case "text-mixed"
|
||||
:text-decoration "text-underlined"
|
||||
:font-weight "text-font-weight"
|
||||
:typography "text-typography"
|
||||
:opacity "percentage"
|
||||
:number "number"
|
||||
:rotation "rotation"
|
||||
:spacing "padding-extended"
|
||||
:string "text-mixed"
|
||||
:stroke-width "stroke-size"
|
||||
:typography "text"
|
||||
:dimensions "expand"
|
||||
:sizing "expand"
|
||||
"add"))
|
||||
|
||||
@@ -80,7 +80,13 @@
|
||||
:sizing "Sizing"
|
||||
:border-radius "Border Radius"
|
||||
: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
|
||||
|
||||
@@ -104,12 +110,23 @@
|
||||
(str/join ", " (map attribute-dictionary 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
|
||||
"Generates a tooltip for a given token"
|
||||
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set]
|
||||
(let [{:keys [name type resolved-value]} token
|
||||
value (cond->> (:value token)
|
||||
(= :font-family type) ctt/join-font-family)
|
||||
(let [{:keys [name type resolved-value value]} token
|
||||
resolved-value-theme (:resolved-value theme-token)
|
||||
resolved-value (or resolved-value-theme resolved-value)
|
||||
{:keys [title] :as token-props} (dwta/get-token-properties theme-token)
|
||||
@@ -125,8 +142,8 @@
|
||||
grouped-values (group-by dimensions-dictionary app-token-keys)
|
||||
|
||||
base-title (dm/str "Token: " name "\n"
|
||||
(tr "workspace.tokens.original-value" value) "\n"
|
||||
(tr "workspace.tokens.resolved-value" resolved-value)
|
||||
(tr "workspace.tokens.original-value" (format-token-value value)) "\n"
|
||||
(tr "workspace.tokens.resolved-value" (format-token-value resolved-value))
|
||||
(when (= (:type token) :number)
|
||||
(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-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/testing "applying any typography token to a shape with a typography style should detach the style"
|
||||
(t/async
|
||||
|
||||
Reference in New Issue
Block a user