mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
🎉 Replace font family form (#7784)
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.main.ui.workspace.tokens.management.create.combobox-token-fonts
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.types.token :as cto]
|
||||||
|
[app.common.types.tokens-lib :as ctob]
|
||||||
|
[app.main.data.style-dictionary :as sd]
|
||||||
|
[app.main.fonts :as fonts]
|
||||||
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
|
[app.main.ui.ds.controls.input :refer [input*]]
|
||||||
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
|
[app.main.ui.forms :as fc]
|
||||||
|
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.forms :as fm]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
|
[clojure.core :as c]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- resolve-value
|
||||||
|
[tokens prev-token value]
|
||||||
|
(let [token
|
||||||
|
{:value (cto/split-font-family value)
|
||||||
|
:name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"}
|
||||||
|
|
||||||
|
tokens
|
||||||
|
(-> tokens
|
||||||
|
;; Remove previous token when renaming a token
|
||||||
|
(dissoc (:name prev-token))
|
||||||
|
(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 (:name token))]
|
||||||
|
(if resolved-value
|
||||||
|
(rx/of {:value resolved-value})
|
||||||
|
(rx/of {:error (first errors)}))))))))
|
||||||
|
|
||||||
|
(mf/defc font-picker-combobox*
|
||||||
|
[{:keys [token tokens name] :rest props}]
|
||||||
|
(let [form (mf/use-ctx fc/context)
|
||||||
|
input-name name
|
||||||
|
|
||||||
|
resolved-input-name
|
||||||
|
(mf/with-memo [input-name]
|
||||||
|
(keyword (str "resolved-" (c/name input-name))))
|
||||||
|
|
||||||
|
touched?
|
||||||
|
(and (contains? (:data @form) input-name)
|
||||||
|
(get-in @form [:touched input-name]))
|
||||||
|
|
||||||
|
error
|
||||||
|
(get-in @form [:errors input-name])
|
||||||
|
|
||||||
|
value
|
||||||
|
(get-in @form [:data input-name] "")
|
||||||
|
|
||||||
|
font (fonts/find-font-family value)
|
||||||
|
|
||||||
|
resolve-stream
|
||||||
|
(mf/with-memo [token]
|
||||||
|
(if-let [value (:value token)]
|
||||||
|
(rx/behavior-subject value)
|
||||||
|
(rx/subject)))
|
||||||
|
|
||||||
|
hint*
|
||||||
|
(mf/use-state {})
|
||||||
|
|
||||||
|
hint
|
||||||
|
(deref hint*)
|
||||||
|
|
||||||
|
font-selector-open* (mf/use-state false)
|
||||||
|
font-selector-open? (deref font-selector-open*)
|
||||||
|
|
||||||
|
on-click-dropdown-button
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps font-selector-open?)
|
||||||
|
(fn [e]
|
||||||
|
(dom/prevent-default e)
|
||||||
|
(reset! font-selector-open* (not font-selector-open?))))
|
||||||
|
|
||||||
|
font-selector-button
|
||||||
|
(mf/html
|
||||||
|
[:> icon-button*
|
||||||
|
{:on-click on-click-dropdown-button
|
||||||
|
:aria-label (tr "workspace.tokens.token-font-family-select")
|
||||||
|
:icon i/arrow-down
|
||||||
|
:variant "action"
|
||||||
|
:type "button"}])
|
||||||
|
|
||||||
|
on-close-font-selector
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(reset! font-selector-open* false)))
|
||||||
|
|
||||||
|
on-select-font
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps font)
|
||||||
|
(fn [{:keys [family] :as font}]
|
||||||
|
(when (not= value family)
|
||||||
|
(fm/on-input-change form input-name family true)
|
||||||
|
(rx/push! resolve-stream family))))
|
||||||
|
|
||||||
|
on-change
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps resolve-stream input-name)
|
||||||
|
(fn [event]
|
||||||
|
(let [value (-> event dom/get-target dom/get-input-value)]
|
||||||
|
(fm/on-input-change form input-name value false)
|
||||||
|
(rx/push! resolve-stream value))))
|
||||||
|
|
||||||
|
props
|
||||||
|
(mf/spread-props props {:on-change on-change
|
||||||
|
;; TODO: Review this value vs default-value
|
||||||
|
:value (or value "")
|
||||||
|
:hint-message (:message hint)
|
||||||
|
:slot-end font-selector-button
|
||||||
|
:hint-type (:type hint)})
|
||||||
|
|
||||||
|
props
|
||||||
|
(if (and error touched?)
|
||||||
|
(mf/spread-props props {:hint-type "error"
|
||||||
|
:hint-message (:message error)})
|
||||||
|
props)]
|
||||||
|
|
||||||
|
(mf/with-effect [resolve-stream tokens token input-name]
|
||||||
|
(let [subs (->> resolve-stream
|
||||||
|
(rx/debounce 300)
|
||||||
|
(rx/mapcat (partial resolve-value tokens token))
|
||||||
|
(rx/map (fn [result]
|
||||||
|
(d/update-when result :error
|
||||||
|
(fn [error]
|
||||||
|
((:error/fn error) (:error/value error))))))
|
||||||
|
(rx/subs! (fn [{:keys [error value]}]
|
||||||
|
(if error
|
||||||
|
(do
|
||||||
|
(swap! form assoc-in [:errors input-name] {:message error})
|
||||||
|
(swap! form assoc-in [:errors resolved-input-name] {:message error})
|
||||||
|
(swap! form update :data dissoc resolved-input-name)
|
||||||
|
(reset! hint* {:message error :type "error"}))
|
||||||
|
(let [message (tr "workspace.tokens.resolved-value" (cto/join-font-family value))]
|
||||||
|
(swap! form update :errors dissoc input-name resolved-input-name)
|
||||||
|
(swap! form update :data assoc resolved-input-name value)
|
||||||
|
(reset! hint* {:message message :type "hint"}))))))]
|
||||||
|
|
||||||
|
(fn []
|
||||||
|
(rx/dispose! subs))))
|
||||||
|
|
||||||
|
[:*
|
||||||
|
[:> input* props]
|
||||||
|
(when font-selector-open?
|
||||||
|
[:div {:class (stl/css :font-select-wrapper)}
|
||||||
|
[:> font-selector* {:current-font font
|
||||||
|
:on-select on-select-font
|
||||||
|
:on-close on-close-font-selector
|
||||||
|
:full-size true}]])]))
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
@use "ds/_utils.scss" as *;
|
||||||
|
|
||||||
|
.font-select-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: px2rem(115);
|
||||||
|
top: px2rem(54);
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.main.ui.workspace.tokens.management.create.font-family
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.files.tokens :as cft]
|
||||||
|
[app.common.schema :as sm]
|
||||||
|
[app.common.types.token :as cto]
|
||||||
|
[app.common.types.tokens-lib :as ctob]
|
||||||
|
[app.main.constants :refer [max-input-length]]
|
||||||
|
[app.main.data.modal :as modal]
|
||||||
|
[app.main.data.workspace.tokens.application :as dwta]
|
||||||
|
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||||
|
[app.main.data.workspace.tokens.propagation :as dwtp]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||||
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
|
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||||
|
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||||
|
[app.main.ui.forms :as fc]
|
||||||
|
[app.main.ui.workspace.tokens.management.create.combobox-token-fonts :refer [font-picker-combobox*]]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.forms :as fm]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
|
[app.util.keyboard :as k]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(defn- make-schema
|
||||||
|
[tokens-tree]
|
||||||
|
(sm/schema
|
||||||
|
[:and
|
||||||
|
[:map
|
||||||
|
[:name
|
||||||
|
[:and
|
||||||
|
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||||
|
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
|
||||||
|
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||||
|
#(not (cft/token-name-path-exists? % tokens-tree))]]]
|
||||||
|
|
||||||
|
[:value ::sm/text]
|
||||||
|
|
||||||
|
[:resolved-value ::sm/any]
|
||||||
|
|
||||||
|
[:description {:optional true}
|
||||||
|
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
|
||||||
|
|
||||||
|
[:fn {:error/field :value
|
||||||
|
:error/fn #(tr "workspace.tokens.self-reference")}
|
||||||
|
(fn [{:keys [name value]}]
|
||||||
|
(when (and name value)
|
||||||
|
(nil? (cto/token-value-self-reference? name value))))]]))
|
||||||
|
|
||||||
|
|
||||||
|
(mf/defc form*
|
||||||
|
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
|
||||||
|
|
||||||
|
(let [token
|
||||||
|
(mf/with-memo [token]
|
||||||
|
(if token
|
||||||
|
(update token :value cto/join-font-family)
|
||||||
|
{:type :font-family}))
|
||||||
|
|
||||||
|
token-type
|
||||||
|
(get token :type)
|
||||||
|
|
||||||
|
token-properties
|
||||||
|
(dwta/get-token-properties token)
|
||||||
|
|
||||||
|
token-title (str/lower (:title token-properties))
|
||||||
|
|
||||||
|
tokens
|
||||||
|
(mf/deref refs/workspace-active-theme-sets-tokens)
|
||||||
|
|
||||||
|
tokens
|
||||||
|
(mf/with-memo [tokens]
|
||||||
|
;; Ensure that the resolved value uses the currently editing token
|
||||||
|
;; even if the name has been overriden by a token with the same name
|
||||||
|
;; in another set below.
|
||||||
|
(cond-> tokens
|
||||||
|
(and (:name token) (:value token))
|
||||||
|
(assoc (:name token) token)))
|
||||||
|
|
||||||
|
schema
|
||||||
|
(mf/with-memo [tokens-tree-in-selected-set]
|
||||||
|
(make-schema tokens-tree-in-selected-set))
|
||||||
|
|
||||||
|
initial
|
||||||
|
(mf/with-memo [token]
|
||||||
|
{:name (:name token "")
|
||||||
|
:value (:value token "")
|
||||||
|
:description (:description token "")})
|
||||||
|
|
||||||
|
form
|
||||||
|
(fm/use-form :schema schema
|
||||||
|
:initial initial)
|
||||||
|
|
||||||
|
warning-name-change?
|
||||||
|
(not= (get-in @form [:data :name])
|
||||||
|
(:name initial))
|
||||||
|
|
||||||
|
on-cancel
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [e]
|
||||||
|
(dom/prevent-default e)
|
||||||
|
(modal/hide!)))
|
||||||
|
|
||||||
|
on-delete-token
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps selected-token-set-id token)
|
||||||
|
(fn [e]
|
||||||
|
(dom/prevent-default e)
|
||||||
|
(modal/hide!)
|
||||||
|
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
|
||||||
|
|
||||||
|
handle-key-down-delete
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-delete-token)
|
||||||
|
(fn [e]
|
||||||
|
(when (or (k/enter? e) (k/space? e))
|
||||||
|
(on-delete-token e))))
|
||||||
|
|
||||||
|
handle-key-down-cancel
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-cancel)
|
||||||
|
(fn [e]
|
||||||
|
(when (or (k/enter? e) (k/space? e))
|
||||||
|
(on-cancel e))))
|
||||||
|
|
||||||
|
on-submit
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps validate-token token tokens token-type)
|
||||||
|
(fn [form _event]
|
||||||
|
(let [name (get-in @form [:clean-data :name])
|
||||||
|
description (get-in @form [:clean-data :description])
|
||||||
|
value (get-in @form [:clean-data :value])]
|
||||||
|
(->> (validate-token {:token-value value
|
||||||
|
:token-name name
|
||||||
|
:token-description description
|
||||||
|
:prev-token token
|
||||||
|
:tokens tokens})
|
||||||
|
(rx/subs!
|
||||||
|
(fn [valid-token]
|
||||||
|
(st/emit!
|
||||||
|
(if is-create
|
||||||
|
(dwtl/create-token (ctob/make-token {:name name
|
||||||
|
:type token-type
|
||||||
|
:value (:value valid-token)
|
||||||
|
:description description}))
|
||||||
|
|
||||||
|
(dwtl/update-token (:id token)
|
||||||
|
{:name name
|
||||||
|
:value (:value valid-token)
|
||||||
|
:description description}))
|
||||||
|
(dwtp/propagate-workspace-tokens)
|
||||||
|
(modal/hide))))))))]
|
||||||
|
|
||||||
|
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||||
|
:form form
|
||||||
|
:on-submit on-submit}
|
||||||
|
[:div {:class (stl/css :token-rows)}
|
||||||
|
|
||||||
|
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||||
|
(tr "workspace.tokens.create-token" token-type)]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :input-row)}
|
||||||
|
[:> fc/form-input* {:id "token-name"
|
||||||
|
:name :name
|
||||||
|
:label (tr "workspace.tokens.token-name")
|
||||||
|
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||||
|
:max-length max-input-length
|
||||||
|
:variant "comfortable"
|
||||||
|
:auto-focus true}]
|
||||||
|
|
||||||
|
(when (and warning-name-change? (= action "edit"))
|
||||||
|
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
|
||||||
|
[:> context-notification*
|
||||||
|
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :input-row)}
|
||||||
|
[:> font-picker-combobox*
|
||||||
|
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||||
|
:label (tr "workspace.tokens.token-value")
|
||||||
|
:name :value
|
||||||
|
:token token
|
||||||
|
:tokens tokens}]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :input-row)}
|
||||||
|
[:> fc/form-input* {:id "token-description"
|
||||||
|
:name :description
|
||||||
|
:label (tr "workspace.tokens.token-description")
|
||||||
|
:placeholder (tr "workspace.tokens.token-description")
|
||||||
|
:max-length max-input-length
|
||||||
|
:variant "comfortable"
|
||||||
|
:is-optional true}]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css-case :button-row true
|
||||||
|
:with-delete (= action "edit"))}
|
||||||
|
(when (= action "edit")
|
||||||
|
[:> button* {:on-click on-delete-token
|
||||||
|
:on-key-down handle-key-down-delete
|
||||||
|
:class (stl/css :delete-btn)
|
||||||
|
:type "button"
|
||||||
|
:icon i/delete
|
||||||
|
:variant "secondary"}
|
||||||
|
(tr "labels.delete")])
|
||||||
|
|
||||||
|
[:> button* {:on-click on-cancel
|
||||||
|
:on-key-down handle-key-down-cancel
|
||||||
|
:type "button"
|
||||||
|
:id "token-modal-cancel"
|
||||||
|
:variant "secondary"}
|
||||||
|
(tr "labels.cancel")]
|
||||||
|
|
||||||
|
[:> fc/form-submit* {:variant "primary"
|
||||||
|
:on-submit on-submit}
|
||||||
|
(tr "labels.save")]]]]))
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
@use "ds/typography.scss" as t;
|
||||||
|
@use "ds/_sizes.scss" as *;
|
||||||
|
@use "ds/_borders.scss" as *;
|
||||||
|
|
||||||
|
.form-wrapper {
|
||||||
|
width: $sz-384;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal-title {
|
||||||
|
@include t.use-typography("headline-medium");
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
justify-content: end;
|
||||||
|
gap: var(--sp-m);
|
||||||
|
padding-block-start: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.with-delete {
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-name-change-notification-wrapper {
|
||||||
|
margin-block-start: var(--sp-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
[app.main.ui.workspace.tokens.management.create.border-radius :as border-radius]
|
[app.main.ui.workspace.tokens.management.create.border-radius :as border-radius]
|
||||||
[app.main.ui.workspace.tokens.management.create.color :as color]
|
[app.main.ui.workspace.tokens.management.create.color :as color]
|
||||||
[app.main.ui.workspace.tokens.management.create.dimensions :as dimensions]
|
[app.main.ui.workspace.tokens.management.create.dimensions :as dimensions]
|
||||||
|
[app.main.ui.workspace.tokens.management.create.font-family :as font-family]
|
||||||
[app.main.ui.workspace.tokens.management.create.input-token-color-bullet :refer [input-token-color-bullet*]]
|
[app.main.ui.workspace.tokens.management.create.input-token-color-bullet :refer [input-token-color-bullet*]]
|
||||||
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token* token-value-hint*]]
|
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token* token-value-hint*]]
|
||||||
[app.main.ui.workspace.tokens.management.create.text-case :as text-case]
|
[app.main.ui.workspace.tokens.management.create.text-case :as text-case]
|
||||||
@@ -1437,13 +1438,14 @@
|
|||||||
(mf/spread-props props {:token-type token-type
|
(mf/spread-props props {:token-type token-type
|
||||||
:validate-token default-validate-token
|
:validate-token default-validate-token
|
||||||
:tokens-tree-in-selected-set tokens-tree-in-selected-set
|
:tokens-tree-in-selected-set tokens-tree-in-selected-set
|
||||||
:token token})]
|
:token token})
|
||||||
|
font-family-props (mf/spread-props props {:validate-token validate-font-family-token})]
|
||||||
|
|
||||||
(case token-type
|
(case token-type
|
||||||
:color [:> color/form* props]
|
:color [:> color/form* props]
|
||||||
:typography [:> typography-form* props]
|
:typography [:> typography-form* props]
|
||||||
:shadow [:> shadow-form* props]
|
:shadow [:> shadow-form* props]
|
||||||
:font-family [:> font-family-form* props]
|
:font-family [:> font-family/form* font-family-props]
|
||||||
:text-case [:> text-case/form* props]
|
:text-case [:> text-case/form* props]
|
||||||
:text-decoration [:> text-decoration-form* props]
|
:text-decoration [:> text-decoration-form* props]
|
||||||
:font-weight [:> font-weight-form* props]
|
:font-weight [:> font-weight-form* props]
|
||||||
|
|||||||
Reference in New Issue
Block a user