From 580bb46a054a64b5706e1ee1655a648e6d139f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Schr=C3=B6dl?= Date: Mon, 23 Jun 2025 12:12:40 +0200 Subject: [PATCH] :sparkles: Implement font-size token type (#6708) * :sparkles: Implement font-size token type * :sparkles: Hide typography tokens behind FF * :lipstick: Update icon * :wrench: Add font-size to unapply check * :recycle: Generalize status-icon logic and remove icon for font-size --- common/src/app/common/flags.cljc | 1 + common/src/app/common/types/token.cljc | 10 ++++ .../common_tests/logic/token_apply_test.cljc | 51 +++++++++++++++---- .../data/workspace/tokens/application.cljs | 32 +++++++++--- .../data/workspace/tokens/propagation.cljs | 1 + .../ui/workspace/tokens/context_menu.cljs | 4 +- .../app/main/ui/workspace/tokens/modals.cljs | 6 +++ .../app/main/ui/workspace/tokens/sidebar.cljs | 14 ++--- .../main/ui/workspace/tokens/token_pill.cljs | 20 +++++--- .../tokens/logic/token_actions_test.cljs | 34 +++++++++++++ 10 files changed, 141 insertions(+), 32 deletions(-) diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index e5304c658b..80f96fd8aa 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -117,6 +117,7 @@ ;; Only for developtment. :tiered-file-data-storage :token-units + :token-typography-types :transit-readable-response :user-feedback ;; TODO: remove this flag. diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index b92b691acc..f4df0dc1c3 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -33,6 +33,7 @@ :border-radius "borderRadius" :color "color" :dimensions "dimension" + :font-size "fontSize" :number "number" :opacity "opacity" :other "other" @@ -122,6 +123,12 @@ (def rotation-keys (schema-keys schema:rotation)) +(def ^:private schema:font-size + [:map + [:font-size {:optional true} token-name-ref]]) + +(def font-size-keys (schema-keys schema:font-size)) + (def ^:private schema:number [:map [:rotation {:optional true} token-name-ref] @@ -137,6 +144,7 @@ spacing-keys dimensions-keys rotation-keys + font-size-keys number-keys)) (def ^:private schema:tokens @@ -150,6 +158,7 @@ schema:spacing schema:rotation schema:number + schema:font-size schema:dimensions]) (defn shape-attr->token-attrs @@ -177,6 +186,7 @@ changed-sub-attr #{:m1 :m2 :m3 :m4}) + (font-size-keys shape-attr) #{shape-attr} (border-radius-keys shape-attr) #{shape-attr} (sizing-keys shape-attr) #{shape-attr} (opacity-keys shape-attr) #{shape-attr} diff --git a/common/test/common_tests/logic/token_apply_test.cljc b/common/test/common_tests/logic/token_apply_test.cljc index 8b028e5ebf..e75a284f85 100644 --- a/common/test/common_tests/logic/token_apply_test.cljc +++ b/common/test/common_tests/logic/token_apply_test.cljc @@ -54,9 +54,13 @@ (ctob/add-token-in-set "test-token-set" (ctob/make-token :name "token-dimensions" :type :dimensions - :value 100)))) + :value 100)) + (ctob/add-token-in-set "test-token-set" + (ctob/make-token :name "token-font-size" + :type :font-size + :value 24)))) (tho/add-frame :frame1) - (tho/add-text :text1 "Hello World"))) + (tho/add-text :text1 "Hello World!"))) (defn- apply-all-tokens [file] @@ -68,19 +72,21 @@ (tht/apply-token-to-shape :frame1 "token-color" [:stroke-color] [:stroke-color] "#00ff00") (tht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00") (tht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100) - (tht/apply-token-to-shape :text1 "token-color" [:fill] [:fill] "#00ff00"))) + (tht/apply-token-to-shape :text1 "token-font-size" [:font-size] [:font-size] 24))) (t/deftest apply-tokens-to-shape (let [;; ==== Setup file (setup-file) page (thf/current-page file) frame1 (ths/get-shape file :frame1) + text1 (ths/get-shape file :text1) token-radius (tht/get-token file "test-token-set" "token-radius") token-rotation (tht/get-token file "test-token-set" "token-rotation") token-opacity (tht/get-token file "test-token-set" "token-opacity") token-stroke-width (tht/get-token file "test-token-set" "token-stroke-width") token-color (tht/get-token file "test-token-set" "token-color") token-dimensions (tht/get-token file "test-token-set" "token-dimensions") + token-font-size (tht/get-token file "test-token-set" "token-font-size") ;; ==== Action changes (-> (-> (pcb/empty-changes nil) @@ -114,13 +120,23 @@ :shape $ :attributes [:width :height]}))) (:objects page) + {}) + (cls/generate-update-shapes [(:id text1)] + (fn [shape] + (as-> shape $ + (cto/maybe-apply-token-to-shape {:token token-font-size + :shape $ + :attributes [:font-size]}))) + (:objects page) {})) file' (thf/apply-changes file changes) ;; ==== Get - frame1' (ths/get-shape file' :frame1) - applied-tokens' (:applied-tokens frame1')] + frame1' (ths/get-shape file' :frame1) + applied-tokens' (:applied-tokens frame1') + text1' (ths/get-shape file' :text1) + text1-applied-tokens (:applied-tokens text1')] ;; ==== Check (t/is (= (count applied-tokens') 11)) @@ -134,7 +150,9 @@ (t/is (= (:stroke-color applied-tokens') "token-color")) (t/is (= (:fill applied-tokens') "token-color")) (t/is (= (:width applied-tokens') "token-dimensions")) - (t/is (= (:height applied-tokens') "token-dimensions")))) + (t/is (= (:height applied-tokens') "token-dimensions")) + (t/is (= (count text1-applied-tokens) 1)) + (t/is (= (:font-size text1-applied-tokens) "token-font-size")))) (t/deftest unapply-tokens-from-shape (let [;; ==== Setup @@ -142,6 +160,7 @@ (apply-all-tokens)) page (thf/current-page file) frame1 (ths/get-shape file :frame1) + text1 (ths/get-shape file :text1) ;; ==== Action changes (-> (-> (pcb/empty-changes nil) @@ -158,16 +177,25 @@ (cto/unapply-token-id [:fill]) (cto/unapply-token-id [:width :height]))) (:objects page) + {}) + (cls/generate-update-shapes [(:id text1)] + (fn [shape] + (-> shape + (cto/unapply-token-id [:font-size]))) + (:objects page) {})) file' (thf/apply-changes file changes) ;; ==== Get - frame1' (ths/get-shape file' :frame1) - applied-tokens' (:applied-tokens frame1')] + frame1' (ths/get-shape file' :frame1) + applied-tokens' (:applied-tokens frame1') + text1' (ths/get-shape file' :text1) + text1-applied-tokens (:applied-tokens text1')] ;; ==== Check - (t/is (= (count applied-tokens') 0)))) + (t/is (= (count applied-tokens') 0)) + (t/is (= (count text1-applied-tokens) 0)))) (t/deftest unapply-tokens-automatic (let [;; ==== Setup @@ -202,7 +230,8 @@ shape txt/is-content-node? d/txt-merge - {:fills (ths/sample-fills-color :fill-color "#fabada")})) + {:fills (ths/sample-fills-color :fill-color "#fabada") + :font-size "1"})) (:objects page) {})) @@ -216,4 +245,4 @@ ;; ==== Check (t/is (= (count applied-tokens-frame') 0)) - (t/is (= (count applied-tokens-text') 0)))) \ No newline at end of file + (t/is (= (count applied-tokens-text') 0)))) diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index c37692d3c5..21c3bc193d 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -22,7 +22,7 @@ [app.main.data.workspace.colors :as wdc] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] - [app.main.data.workspace.transforms :as dwt] + [app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.undo :as dwu] [app.main.store :as st] [beicon.v2.core :as rx] @@ -238,8 +238,8 @@ (watch [_ _ _] (when (number? value) (rx/of - (when (:width attributes) (dwt/update-dimensions shape-ids :width value {:ignore-touched true :page-id page-id})) - (when (:height attributes) (dwt/update-dimensions shape-ids :height value {:ignore-touched true :page-id page-id})))))))) + (when (:width attributes) (dwtr/update-dimensions shape-ids :width value {:ignore-touched true :page-id page-id})) + (when (:height attributes) (dwtr/update-dimensions shape-ids :height value {:ignore-touched true :page-id page-id})))))))) (defn- attributes->layout-gap [attributes value] (let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap}) @@ -311,9 +311,9 @@ (when (number? value) (let [page-id (or page-id (get state :current-page-id))] (->> (rx/from shape-ids) - (rx/map #(dwt/update-position % (zipmap attributes (repeat value)) - {:ignore-touched true - :page-id page-id}))))))))) + (rx/map #(dwtr/update-position % (zipmap attributes (repeat value)) + {:ignore-touched true + :page-id page-id}))))))))) (defn update-layout-sizing-limits ([value shape-ids attributes] (update-layout-sizing-limits value shape-ids attributes nil)) @@ -346,6 +346,18 @@ {:ignore-touched true :page-id page-id}))))) +(defn update-font-size + ([value shape-ids attributes] (update-font-size value shape-ids attributes nil)) + ([value shape-ids _attributes page-id] + (let [update-node? (fn [node] + (or (txt/is-text-node? node) + (txt/is-paragraph-node? node)))] + (when (number? value) + (dwsh/update-shapes shape-ids + #(txt/update-text-content % update-node? d/txt-merge {:font-size (str value)}) + {:ignore-touched true + :page-id page-id}))))) + ;; Map token types to different properties used along the cokde --------------------------------------------- ;; FIXME: the values should be lazy evaluated, probably a function, @@ -371,6 +383,14 @@ :modal {:key :tokens/color :fields [{:label "Color" :key :color}]}} + :font-size + {:title "Font Size" + :attributes ctt/font-size-keys + :on-update-shape update-font-size + :modal {:key :tokens/font-size + :fields [{:label "Font Size" + :key :font-size}]}} + :stroke-width {:title "Stroke Width" :attributes ctt/stroke-width-keys diff --git a/frontend/src/app/main/data/workspace/tokens/propagation.cljs b/frontend/src/app/main/data/workspace/tokens/propagation.cljs index 07ea4c39ad..237e8d2c69 100644 --- a/frontend/src/app/main/data/workspace/tokens/propagation.cljs +++ b/frontend/src/app/main/data/workspace/tokens/propagation.cljs @@ -33,6 +33,7 @@ ctt/sizing-keys dwta/update-shape-dimensions ctt/opacity-keys dwta/update-opacity #{:line-height} dwta/update-line-height + #{:font-size} dwta/update-font-size #{:x :y} dwta/update-shape-position #{:p1 :p2 :p3 :p4} dwta/update-layout-padding #{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs index fe29e775bb..a6ce607284 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs @@ -233,7 +233,8 @@ (dwta/update-shape-radius-for-corners value shape-ids attributes))) (def shape-attribute-actions-map - (let [stroke-width (partial generic-attribute-actions #{:stroke-width} "Stroke Width")] + (let [stroke-width (partial generic-attribute-actions #{:stroke-width} "Stroke Width") + font-size (partial generic-attribute-actions #{:font-size} "Font Size")] {:border-radius (partial all-or-separate-actions {:attribute-labels {:r1 "Top Left" :r2 "Top Right" :r4 "Bottom Left" @@ -252,6 +253,7 @@ [(generic-attribute-actions #{:rotation} "Rotation" (assoc context-data :on-update-shape dwta/update-rotation)) (generic-attribute-actions #{:line-height} "Line Height" (assoc context-data :on-update-shape dwta/update-line-height))]) :stroke-width stroke-width + :font-size font-size :dimensions (fn [context-data] (concat [{:title "Sizing" :submenu :sizing} diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/modals.cljs index 4b96483eb5..9f7f8594d8 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals.cljs @@ -179,3 +179,9 @@ ::mf/register-as :tokens/typography} [properties] [:& token-update-create-modal properties]) + +(mf/defc font-size-modal + {::mf/register modal/components + ::mf/register-as :tokens/font-size} + [properties] + [:& token-update-create-modal properties]) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index f011824f9d..b628033d6c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -50,6 +50,7 @@ :border-radius "corner-radius" :color "drop" :boolean "boolean-difference" + :font-size "text-font-size" :opacity "percentage" :number "number" :rotation "rotation" @@ -145,14 +146,15 @@ tokens exist for that type. Sort each group alphabetically (by their type). If `:token-units` is not in cf/flags, number tokens are excluded." [tokens-by-type] - (let [all-types (-> dwta/token-properties keys seq) - token-units? (contains? cf/flags :token-units) - filtered-types (if token-units? - all-types - (remove #(= % :number) all-types))] + (let [token-units? (contains? cf/flags :token-units) + token-typography-types? (contains? cf/flags :token-typography-types) + all-types (cond-> dwta/token-properties + (not token-units?) (dissoc :number) + (not token-typography-types?) (dissoc :font-size)) + all-types (-> all-types keys seq)] (loop [empty #js [] filled #js [] - types filtered-types] + types all-types] (if-let [type (first types)] (if (not-empty (get tokens-by-type type)) (recur empty diff --git a/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs b/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs index 1bda279c0e..fe532abacd 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs @@ -162,6 +162,9 @@ shape-ids (into #{} xf:map-id selected-shapes)] (cft/shapes-applied-all? ids-by-attributes shape-ids attributes))) +(def token-types-with-status-icon + #{:color :border-radius :rotation :sizing :dimensions :opacity :spacing :stroke-width}) + (mf/defc token-pill* {::mf/wrap [mf/memo]} [{:keys [on-click token on-context-menu selected-shapes active-theme-tokens]}] @@ -208,7 +211,7 @@ (or (dwtc/resolved-token-bullet-color theme-token) (dwtc/resolved-token-bullet-color token)))) - number-token (= type :number) + status-icon? (contains? token-types-with-status-icon type) on-click (mf/use-fn @@ -255,7 +258,7 @@ [:button {:class (stl/css-case :token-pill true - :token-pill-no-icon (and number-token (not errors?)) + :token-pill-no-icon (and (not status-icon?) (not errors?)) :token-pill-default can-edit? :token-pill-applied (and can-edit? has-selected? (or half-applied? full-applied?)) :token-pill-invalid (and can-edit? errors?) @@ -280,12 +283,13 @@ {:icon-id "broken-link" :class (stl/css :token-pill-icon)}] - (not number-token) - (if color - [:& color-bullet {:color color :mini true}] - [:> token-status-icon* - {:icon-id token-status-id - :class (stl/css :token-pill-icon)}])) + color + [:& color-bullet {:color color :mini true}] + + status-icon? + [:> token-status-icon* + {:icon-id token-status-id + :class (stl/css :token-pill-icon)}]) (if contains-path? (let [[first-part last-part] (cfh/split-by-last-period name)] diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs index 6842b24bd9..b717fb8b3d 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -446,6 +446,40 @@ (t/is (= (:stroke-width (:applied-tokens rect-without-stroke')) (:name token-target'))) (t/is (empty? (:strokes rect-without-stroke'))))))))))) +(t/deftest test-apply-font-size + (t/testing "applies font-size token and updates the text font-size" + (t/async + done + (let [font-size-token {:name "heading-size" + :value "24" + :type :font-size} + file (-> (setup-file-with-tokens) + (update-in [:data :tokens-lib] + #(ctob/add-token-in-set % "Set A" (ctob/make-token font-size-token)))) + store (ths/setup-store file) + text-1 (cths/get-shape file :text-1) + events [(dwta/apply-token {:shape-ids [(:id text-1)] + :attributes #{:font-size} + :token (toht/get-token file "heading-size") + :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) + token-target' (toht/get-token file' "heading-size") + 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 (= (:font-size (:applied-tokens text-1')) (:name token-target'))) + (t/is (= (:font-size style-text-blocks) "24"))))))))) + (t/deftest test-apply-line-height (t/testing "applies line-height token and updates the text line-height" (t/async