diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index bb1c7b54cd..3deffead5f 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -10,6 +10,7 @@ [app.common.files.tokens :as cft] [app.common.types.shape.layout :as ctsl] [app.common.types.shape.radius :as ctsr] + [app.common.types.shape.token :as ctst] [app.common.types.stroke :as cts] [app.common.types.text :as txt] [app.common.types.token :as ctt] @@ -284,53 +285,71 @@ (when (number? value) (generate-text-shape-update {:letter-spacing (str value)} shape-ids page-id)))) -(defn- generate-font-variant-text-shape-update - "Generate shape update for either updating `:font-family` or `font-weight`. - Try to find the closest weight variant." +(defn warn-font-variant-not-found! [] + (st/emit! + (ntf/show {:content (tr "workspace.tokens.font-variant-not-found") + :type :toast + :level :warning + :timeout 7000}))) + +(defn- update-closest-font-variant-id-by-weight + [txt-attrs target-variant font-id on-mismatch] + (let [font (fonts/get-font-data font-id) + variant (when font + (fonts/find-closest-variant font (:weight target-variant) (:style target-variant))) + call-on-mismatch? (when (and (fn? on-mismatch) variant) + (or + (not= (:font-weight target-variant) (:weight variant)) + (when (:font-style target-variant) + (not= (:font-style target-variant) (:style variant)))))] + (when call-on-mismatch? + (on-mismatch)) + (cond-> txt-attrs + (:id variant) (assoc :font-variant-id (:id variant))))) + +(defn- generate-font-family-text-shape-update [txt-attrs shape-ids page-id on-mismatch] - (let [update-node? (fn [node] + (let [not-found-font (= (:font-id txt-attrs) (str uuid/zero)) + update-node? (fn [node] (or (txt/is-text-node? node) (txt/is-paragraph-node? node))) - update-fn (fn [node _] - (let [font (if (= (:font-id txt-attrs) (str uuid/zero)) - (fonts/get-font-data (:font-id node)) - (fonts/get-font-data (:font-id txt-attrs))) - variant (when font - (fonts/find-closest-variant font (:weight node) (:style node))) + update-fn (fn [node find-closest-weight?] + (let [font-id (if not-found-font (:font-id node) (:font-id txt-attrs)) txt-attrs (cond-> txt-attrs - (:id variant) (assoc :font-variant-id (:id variant))) - call-on-mismatch? (when (and (fn? on-mismatch) variant) - (or - (not= (:weight txt-attrs) (:weight variant)) - (when (:style txt-attrs) - (not= (:style txt-attrs) (:style variant)))))] - (if (or variant (not font)) - (do - (when call-on-mismatch? (on-mismatch variant)) - (-> node - (d/txt-merge txt-attrs) - (cty/remove-typography-from-node))) - node)))] + find-closest-weight? (update-closest-font-variant-id-by-weight node font-id on-mismatch))] + (-> node + (d/txt-merge txt-attrs) + (cty/remove-typography-from-node))))] (dwsh/update-shapes shape-ids - #(txt/update-text-content % update-node? update-fn nil) + (fn [shape] + (txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil)) {:ignore-touched true :page-id page-id}))) +(defn- create-font-family-text-attrs + [value] + (let [font-family (-> (first value) + ;; Strip quotes around font-family like `"Inter"` + (str/trim #"[\"']")) + font (some-> font-family + (fonts/find-font-family))] + (if font + {:font-id (:id font) + :font-family (:family font)} + {:font-id (str uuid/zero) + :font-family font-family}))) + (defn update-font-family ([value shape-ids attributes] (update-font-family value shape-ids attributes nil)) ([value shape-ids _attributes page-id] - (let [font-family (-> (first value) - ;; Strip quotes around font-family like `"Inter"` - (str/trim #"[\"']")) - font-family (some-> font-family - (fonts/find-font-family)) - text-attrs (if font-family - {:font-id (:id font-family) - :font-family (:family font-family)} - {:font-id (str uuid/zero) - :font-family font-family})] - (when text-attrs - (generate-font-variant-text-shape-update text-attrs shape-ids page-id nil))))) + (when-let [text-attrs (create-font-family-text-attrs value)] + (generate-font-family-text-shape-update text-attrs shape-ids page-id nil)))) + +(defn update-font-family-interactive + ([value shape-ids attributes] (update-font-family-interactive value shape-ids attributes nil)) + ([value shape-ids _attributes page-id] + (when-let [text-attrs (create-font-family-text-attrs value)] + (generate-font-family-text-shape-update text-attrs shape-ids page-id warn-font-variant-not-found!)))) (defn update-font-size ([value shape-ids attributes] (update-font-size value shape-ids attributes nil)) @@ -360,22 +379,35 @@ (st/emit! (ptk/data-event :expand-text-more-options)) (update-text-decoration value shape-ids attributes page-id)))) +(defn- generate-font-weight-text-shape-update + [font-variant shape-ids page-id on-mismatch] + (let [font-variant (assoc font-variant + :font-weight (:weight font-variant) + :font-style (:style font-variant)) + update-node? (fn [node] + (or (txt/is-text-node? node) + (txt/is-paragraph-node? node))) + update-fn (fn [node _] + (let [txt-attrs (update-closest-font-variant-id-by-weight font-variant font-variant (:font-id node) on-mismatch)] + (-> node + (d/txt-merge txt-attrs) + (cty/remove-typography-from-node))))] + (dwsh/update-shapes shape-ids + #(txt/update-text-content % update-node? update-fn nil) + {:ignore-touched true + :page-id page-id}))) (defn update-font-weight ([value shape-ids attributes] (update-font-weight value shape-ids attributes nil)) ([value shape-ids _attributes page-id] (when-let [font-variant (ctt/valid-font-weight-variant value)] - (generate-font-variant-text-shape-update font-variant shape-ids page-id nil)))) + (generate-font-weight-text-shape-update font-variant shape-ids page-id nil)))) (defn update-font-weight-interactive ([value shape-ids attributes] (update-font-weight-interactive value shape-ids attributes nil)) ([value shape-ids _attributes page-id] (when-let [font-variant (ctt/valid-font-weight-variant value)] - (let [on-mismatch #(st/emit! (ntf/show {:content (tr "workspace.tokens.font-variant-not-found") - :type :toast - :level :warning - :timeout 7000}))] - (generate-font-variant-text-shape-update font-variant shape-ids page-id on-mismatch))))) + (generate-font-weight-text-shape-update font-variant shape-ids page-id warn-font-variant-not-found!)))) (defn- apply-functions-map "Apply map of functions `fs` to a map of values `vs` using `args`. @@ -404,6 +436,21 @@ value [shape-ids attributes page-id]))))) +(defn update-typography-interactive + ([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-interactive + :font-weight update-font-weight-interactive + :letter-spacing update-letter-spacing + :text-case update-text-case + :text-decoration update-text-decoration-interactive} + value + [shape-ids attributes page-id]))))) + ;; Events to apply / unapply tokens to shapes ------------------------------------------------------------ (defn apply-token @@ -579,7 +626,7 @@ :font-family {:title "Font Family" :attributes ctt/font-family-keys - :on-update-shape update-font-family + :on-update-shape update-font-family-interactive :modal {:key :tokens/font-family :fields [{:label "Font Family" :key :font-family}]}} diff --git a/frontend/src/app/main/data/workspace/tokens/propagation.cljs b/frontend/src/app/main/data/workspace/tokens/propagation.cljs index a30cd5ca4f..a2b6aa89b5 100644 --- a/frontend/src/app/main/data/workspace/tokens/propagation.cljs +++ b/frontend/src/app/main/data/workspace/tokens/propagation.cljs @@ -22,38 +22,6 @@ [clojure.set :as set] [potok.v2.core :as ptk])) -;; Constants ------------------------------------------------------------------- - -(def ^:private filter-existing-values? false) - -(def ^:private attributes->shape-update - {ctt/border-radius-keys dwta/update-shape-radius-for-corners - ctt/color-keys dwta/update-fill-stroke - ctt/stroke-width-keys dwta/update-stroke-width - 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 - #{:letter-spacing} dwta/update-letter-spacing - #{:font-family} dwta/update-font-family - #{: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 - #{:column-gap :row-gap} dwta/update-layout-spacing - #{:width :height} dwta/update-shape-dimensions - #{:layout-item-min-w :layout-item-min-h :layout-item-max-w :layout-item-max-h} dwta/update-layout-sizing-limits - ctt/rotation-keys dwta/update-rotation}) - -(def attribute-actions-map - (reduce - (fn [acc [ks action]] - (into acc (map (fn [k] [k action]) ks))) - {} attributes->shape-update)) - ;; Helpers --------------------------------------------------------------------- ;; TODO: see if this can be replaced by more standard functions @@ -67,6 +35,56 @@ ([a b & rest] (reduce deep-merge a (cons b rest)))) +(defn- flatten-set-keyed-map + "Flattens a map where the keys are sets of keywords." + [m into-m] + (reduce + (fn [acc [ks action]] + (into acc (map (fn [k] [k action]) ks))) + into-m m)) + +;; Constants ------------------------------------------------------------------- + +(def ^:private filter-existing-values? false) + +(def ^:private attributes->shape-update + {ctt/border-radius-keys dwta/update-shape-radius-for-corners + ctt/color-keys dwta/update-fill-stroke + ctt/stroke-width-keys dwta/update-stroke-width + ctt/sizing-keys dwta/update-shape-dimensions + ctt/opacity-keys dwta/update-opacity + ctt/rotation-keys dwta/update-rotation + + ;; Typography + ctt/font-family-keys dwta/update-font-family + ctt/font-size-keys dwta/update-font-size + ctt/font-weight-keys dwta/update-font-weight + ctt/letter-spacing-keys dwta/update-letter-spacing + ctt/text-case-keys dwta/update-text-case + ctt/text-decoration-keys dwta/update-text-decoration + ctt/typography-token-keys dwta/update-typography + #{:line-height} dwta/update-line-height + + ;; Layout + #{:x :y} dwta/update-shape-position + #{:p1 :p2 :p3 :p4} dwta/update-layout-padding + #{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin + #{:column-gap :row-gap} dwta/update-layout-spacing + #{:width :height} dwta/update-shape-dimensions + #{:layout-item-min-w :layout-item-min-h :layout-item-max-w :layout-item-max-h} dwta/update-layout-sizing-limits}) + +(def ^:private attribute-actions-map + (flatten-set-keyed-map attributes->shape-update {})) + +(def ^:private interactive-attributes->shape-update + {ctt/font-family-keys dwta/update-font-family-interactive + ctt/font-weight-keys dwta/update-font-weight-interactive + ctt/text-decoration-keys dwta/update-text-decoration-interactive + ctt/typography-token-keys dwta/update-typography-interactive}) + +(def ^:private interactive-attribute-actions-map + (flatten-set-keyed-map interactive-attributes->shape-update attribute-actions-map)) + ;; Data flows ------------------------------------------------------------------ (defn- invert-collect-key-vals @@ -130,19 +148,26 @@ [tokens frame-ids text-ids]))) -(defn- actionize-shapes-update-info [page-id shapes-update-info] - (mapcat (fn [[attrs update-infos]] - (let [action (some attribute-actions-map attrs)] - (assert (fn? action) "missing action function on attributes->shape-update") - (map - (fn [[v shape-ids]] - (action v shape-ids attrs page-id)) - update-infos))) - shapes-update-info)) +(defn- actionize-shapes-update-info + [page-id shapes-update-info interactive?] + (let [attribute-actions (if interactive? + interactive-attribute-actions-map + attribute-actions-map)] + (mapcat (fn [[attrs update-infos]] + (let [action (some attribute-actions attrs)] + (assert (fn? action) "missing action function on attributes->shape-update") + (map + (fn [[v shape-ids]] + (action v shape-ids attrs page-id)) + update-infos))) + shapes-update-info))) (defn propagate-tokens - "Propagate tokens values to all shapes where they are applied" - [state resolved-tokens] + "Propagate tokens values to all shapes where they are applied + + Pass `interactive?` to indicate the propagation was triggered by a user interaction + and should use update functions that may execute ui side-effects like showing warnings." + [state resolved-tokens interactive?] (let [file-id (get state :current-file-id) current-page-id (get state :current-page-id) fdata (dsh/lookup-file-data state file-id) @@ -162,7 +187,7 @@ (collect-shapes-update-info resolved-tokens (:objects page)) actions - (actionize-shapes-update-info page-id attrs) + (actionize-shapes-update-info page-id attrs interactive?) ;; Composed updates return observables and need to be executed differently {:keys [observable normal]} (group-by #(if (rx/observable? %) :observable :normal) actions)] @@ -193,7 +218,11 @@ (l/inf :status "END" :hint "propagate-tokens" :elapsed elapsed))))))) (defn propagate-workspace-tokens - [] + "Updates styles for tokens. + + Pass `interactive?` to indicate the propagation was triggered by a user interaction + and should use update functions that may execute ui side-effects like showing warnings." + [& {:keys [interactive?]}] (ptk/reify ::propagate-workspace-tokens ptk/WatchEvent (watch [_ state _] @@ -205,5 +234,5 @@ (let [undo-id (js/Symbol)] (rx/concat (rx/of (dwu/start-undo-transaction undo-id :timeout false)) - (propagate-tokens state sd-tokens) + (propagate-tokens state sd-tokens interactive?) (rx/of (dwu/commit-undo-transaction undo-id))))))))))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs index bfef1b54a8..3d4eabef02 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs @@ -469,7 +469,7 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va {:name final-name :value (:value valid-token) :description final-description})) - (dwtp/propagate-workspace-tokens) + (dwtp/propagate-workspace-tokens :interactive? true) (modal/hide))))))))) on-delete-token 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 9345f21997..1329c9ea8b 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -908,7 +908,7 @@ (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-weight style-text-blocks) "700")) (t/is (= (:font-family style-text-blocks) "sourcesanspro")) (t/is (= (:letter-spacing style-text-blocks) "2")) (t/is (= (:text-transform style-text-blocks) "uppercase"))