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 2652c35395..c391e7bf65 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 @@ -11,7 +11,6 @@ [app.common.data.macros :as dm] [app.common.files.tokens :as cft] [app.common.schema :as sm] - [app.common.types.color :as c] [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] [app.main.constants :refer [max-input-length]] @@ -22,22 +21,19 @@ [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.fonts :as fonts] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.ds.buttons.button :refer [button*]] - [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.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] - [app.main.ui.icons :as deprecated-icon] - [app.main.ui.workspace.colorpicker :as colorpicker] - [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-token* token-value-hint*]] + [app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]] + [app.main.ui.workspace.tokens.management.create.shadow :refer [shadow-value-inputs*]] + [app.main.ui.workspace.tokens.management.create.shared.color-picker :refer [color-picker*]] + [app.main.ui.workspace.tokens.management.create.shared.composite-tabs :refer [composite-tabs*]] + [app.main.ui.workspace.tokens.management.create.shared.font-combobox :refer [font-picker-combobox*]] + [app.main.ui.workspace.tokens.management.create.typography-composite :refer [typography-value-inputs*]] [app.util.dom :as dom] [app.util.functions :as uf] [app.util.i18n :refer [tr]] @@ -659,104 +655,6 @@ :disabled disabled?} (tr "labels.save")]]]])) -;; Tabs Component -------------------------------------------------------------- - -(mf/defc composite-reference-input* - [{:keys [default-value on-blur on-update-value token-resolve-result reference-label reference-icon is-reference-fn]}] - [:> input-token* - {:aria-label (tr "labels.reference") - :placeholder reference-label - :icon reference-icon - :default-value (when (is-reference-fn default-value) default-value) - :on-blur on-blur - :on-change on-update-value - :token-resolve-result (when (or - (:errors token-resolve-result) - (string? (:value token-resolve-result))) - token-resolve-result)}]) - -(mf/defc composite-tabs* - [{:keys [default-value - on-update-value - on-external-update-value - on-value-resolve - clear-resolve-value - custom-input-token-value-props] - :rest props}] - (let [;; Active Tab State - {:keys [active-tab - composite-tab - is-reference-fn - reference-icon - reference-label - set-active-tab - title - update-composite-backup-value]} custom-input-token-value-props - reference-tab-active? (= :reference active-tab) - ;; Backup value ref - ;; Used to restore the previously entered value when switching tabs - ;; Uses ref to not trigger state updates during update - backup-state-ref (mf/use-var - (if reference-tab-active? - {:reference default-value} - {:composite default-value})) - default-value (get @backup-state-ref active-tab) - - on-toggle-tab - (mf/use-fn - (mf/deps active-tab on-external-update-value on-value-resolve clear-resolve-value) - (fn [] - (let [next-tab (if (= active-tab :composite) :reference :composite)] - ;; Clear the resolved value so it wont show up before the next-tab value has resolved - (clear-resolve-value) - ;; Restore the internal value from backup - (on-external-update-value (get @backup-state-ref next-tab)) - (set-active-tab next-tab)))) - - update-composite-value - (mf/use-fn - (fn [f] - (clear-resolve-value) - (swap! backup-state-ref f) - (on-external-update-value (get @backup-state-ref :composite)))) - - ;; Store updated value in backup-state-ref - on-update-value' - (mf/use-fn - (mf/deps on-update-value reference-tab-active? update-composite-backup-value) - (fn [e] - (if reference-tab-active? - (swap! backup-state-ref assoc :reference (dom/get-target-val e)) - (swap! backup-state-ref update :composite #(update-composite-backup-value % e))) - (on-update-value e)))] - [:div {:class (stl/css :typography-inputs-row)} - [:div {:class (stl/css :title-bar)} - [:div {:class (stl/css :title)} title] - [:& radio-buttons {:class (stl/css :listing-options) - :selected (if reference-tab-active? "reference" "composite") - :on-change on-toggle-tab - :name "reference-composite-tab"} - [:& radio-button {:icon deprecated-icon/layers - :value "composite" - :title (tr "workspace.tokens.individual-tokens") - :id "composite-opt"}] - [:& radio-button {:icon deprecated-icon/tokens - :value "reference" - :title (tr "workspace.tokens.use-reference") - :id "reference-opt"}]]] - [:div {:class (stl/css :typography-inputs)} - (if reference-tab-active? - [:> composite-reference-input* - (mf/spread-props props {:default-value default-value - :on-update-value on-update-value' - :reference-icon reference-icon - :reference-label reference-label - :is-reference-fn is-reference-fn})] - [:> composite-tab - (mf/spread-props props {:default-value default-value - :on-update-value on-update-value' - :update-composite-value update-composite-value})])]])) - (mf/defc composite-form* "Wrapper around form* that manages composite/reference tab state. Takes the same props as form* plus a function to determine if a token value is a reference." @@ -793,124 +691,6 @@ ;; Token Type Forms ------------------------------------------------------------ -;; FIXME: this function has confusing name -(defn- hex->value - [hex] - (when-let [tc (tinycolor/valid-color hex)] - (let [hex (tinycolor/->hex-string tc) - alpha (tinycolor/alpha tc) - [r g b] (c/hex->rgb hex) - [h s v] (c/hex->hsv hex)] - {:hex hex - :r r :g g :b b - :h h :s s :v v - :alpha alpha}))) - -(mf/defc ramp* - [{:keys [color on-change]}] - (let [wrapper-node-ref (mf/use-ref nil) - dragging-ref (mf/use-ref false) - - on-start-drag - (mf/use-fn #(mf/set-ref-val! dragging-ref true)) - - on-finish-drag - (mf/use-fn #(mf/set-ref-val! dragging-ref false)) - - internal-color* - (mf/use-state #(hex->value color)) - - internal-color - (deref internal-color*) - - on-change' - (mf/use-fn - (mf/deps on-change) - (fn [{:keys [hex alpha] :as selector-color}] - (let [dragging? (mf/ref-val dragging-ref)] - (when-not (and dragging? hex) - (reset! internal-color* selector-color) - (on-change hex alpha)))))] - (mf/use-effect - (mf/deps color) - (fn [] - ;; Update internal color when user changes input value - (when-let [color (tinycolor/valid-color color)] - (when-not (= (tinycolor/->hex-string color) (:hex internal-color)) - (reset! internal-color* (hex->value color)))))) - - (colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color) - [:div {:ref wrapper-node-ref} - [:> ramp-selector* - {:color internal-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag - :on-change on-change'}]])) - -(mf/defc color-picker* - [{: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*) - - on-click-swatch - (mf/use-fn - (mf/deps color-ramp-open? on-display-colorpicker) - (fn [] - (let [open? (not color-ramp-open?)] - (reset! color-ramp-open* open?) - (when on-display-colorpicker - (on-display-colorpicker open?))))) - - swatch - (mf/html - [:> input-token-color-bullet* - {:color color - :class (stl/css :slot-start) - :on-click on-click-swatch}]) - - on-change' - (mf/use-fn - (mf/deps color on-external-update-value) - (fn [hex-value alpha] - (let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field - prev-input-color (some-> (dom/get-value (mf/ref-val input-ref)) - (tinycolor/valid-color)) - ;; If the input is a reference we will take the format from the computed value - prev-computed-color (when-not prev-input-color - (some-> color (tinycolor/valid-color))) - prev-format (some-> (or prev-input-color prev-computed-color) - (tinycolor/color-format)) - to-rgba? (and - (< alpha 1) - (or (= prev-format "hex") (not prev-format))) - to-hex? (and (not prev-format) (= alpha 1)) - format (cond - to-rgba? "rgba" - to-hex? "hex" - prev-format prev-format - :else "hex") - color-value (-> (tinycolor/valid-color hex-value) - (tinycolor/set-alpha (or alpha 1)) - (tinycolor/->string format))] - (dom/set-value! (mf/ref-val input-ref) color-value) - (on-external-update-value color-value))))] - - [:* - [:> input-token* - {:placeholder placeholder - :label label - :default-value default-value - :ref input-ref - :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'}]) - [:> token-value-hint* {:result token-resolve-result}]])) - (mf/defc color-form* [{:keys [token on-display-colorpicker] :rest props}] (let [color* (mf/use-state (:value token)) @@ -945,238 +725,6 @@ :custom-input-token-value color-picker* :custom-input-token-value-props custom-input-token-value-props})])) -(mf/defc shadow-color-picker-wrapper* - "Wrapper for color-picker* that passes shadow color state from parent. - Similar to color-form* but receives color state from shadow-value-inputs*." - [{:keys [placeholder label default-value input-ref on-update-value on-external-update-value token-resolve-result shadow-color]}] - (let [;; Use the color state passed from parent (shadow-value-inputs*) - resolved-color (get token-resolve-result :resolved-value) - color (or shadow-color resolved-color default-value "") - - custom-input-token-value-props - (mf/use-memo - (mf/deps color) - (fn [] - {:color color}))] - - [:> color-picker* - {:placeholder placeholder - :label label - :default-value default-value - :input-ref input-ref - :on-update-value on-update-value - :on-external-update-value on-external-update-value - :custom-input-token-value-props custom-input-token-value-props - :token-resolve-result token-resolve-result}])) - -(def ^:private shadow-inputs - #(d/ordered-map - :offsetX - {:label (tr "workspace.tokens.shadow-x") - :placeholder (tr "workspace.tokens.shadow-x")} - :offsetY - {:label (tr "workspace.tokens.shadow-y") - :placeholder (tr "workspace.tokens.shadow-y")} - :blur - {:label (tr "workspace.tokens.shadow-blur") - :placeholder (tr "workspace.tokens.shadow-blur")} - :spread - {:label (tr "workspace.tokens.shadow-spread") - :placeholder (tr "workspace.tokens.shadow-spread")} - :color - {:label (tr "workspace.tokens.shadow-color") - :placeholder (tr "workspace.tokens.shadow-color")} - :inset - {:label (tr "workspace.tokens.shadow-inset") - :placeholder (tr "workspace.tokens.shadow-inset")})) - -(mf/defc inset-type-select* - [{:keys [default-value shadow-idx label on-change]}] - (let [selected* (mf/use-state (or (str default-value) "false")) - selected (deref selected*) - - on-change - (mf/use-fn - (mf/deps on-change selected shadow-idx) - (fn [value e] - (obj/set! e "tokenValue" (if (= "true" value) true false)) - (on-change e) - (reset! selected* (str value))))] - [:div {:class (stl/css :input-row)} - [:div {:class (stl/css :inset-label)} label] - [:& radio-buttons {:selected selected - :on-change on-change - :name (str "inset-select-" shadow-idx)} - [:& radio-button {:value "false" - :title "false" - :icon "❌" - :id (str "inset-default-" shadow-idx)}] - [:& radio-button {:value "true" - :title "true" - :icon "✅" - :id (str "inset-false-" shadow-idx)}]]])) - -(mf/defc shadow-input* - [{:keys [default-value label placeholder shadow-idx input-type on-update-value on-external-update-value token-resolve-result errors-by-key shadow-color]}] - (let [color-input-ref (mf/use-ref) - - on-change - (mf/use-fn - (mf/deps shadow-idx input-type on-update-value) - (fn [e] - (-> (obj/set! e "tokenTypeAtIndex" [shadow-idx input-type]) - (on-update-value)))) - - on-external-update-value' - (mf/use-fn - (mf/deps shadow-idx input-type on-external-update-value) - (fn [v] - (on-external-update-value [shadow-idx input-type] v))) - - resolved (get-in token-resolve-result [:resolved-value shadow-idx input-type]) - - errors (get errors-by-key input-type) - - should-show? (or (some? resolved) (seq errors)) - - token-prop (when should-show? - (d/without-nils - {:resolved-value resolved - :errors errors}))] - (case input-type - :inset - [:> inset-type-select* - {:default-value default-value - :shadow-idx shadow-idx - :label label - :on-change on-change}] - :color - [:> shadow-color-picker-wrapper* - {:placeholder placeholder - :label label - :default-value default-value - :input-ref color-input-ref - :on-update-value on-change - :on-external-update-value on-external-update-value' - :token-resolve-result token-prop - :shadow-color shadow-color - :data-testid (str "shadow-color-input-" shadow-idx)}] - [:div {:class (stl/css :input-row) - :data-testid (str "shadow-" (name input-type) "-input-" shadow-idx)} - [:> input-token* - {:label label - :placeholder placeholder - :default-value default-value - :on-change on-change - :token-resolve-result token-prop}]]))) - -(mf/defc shadow-input-fields* - [{:keys [shadow shadow-idx on-remove-shadow on-add-shadow is-remove-disabled on-update-value token-resolve-result errors-by-key on-external-update-value shadow-color] :as props}] - (let [on-remove-shadow - (mf/use-fn - (mf/deps shadow-idx on-remove-shadow) - #(on-remove-shadow shadow-idx))] - [:div {:data-testid (str "shadow-input-fields-" shadow-idx)} - [:> icon-button* {:icon i/add - :type "button" - :on-click on-add-shadow - :data-testid (str "shadow-add-button-" shadow-idx) - :aria-label (tr "workspace.tokens.shadow-add-shadow")}] - [:> icon-button* {:variant "ghost" - :type "button" - :icon i/remove - :on-click on-remove-shadow - :disabled is-remove-disabled - :data-testid (str "shadow-remove-button-" shadow-idx) - :aria-label (tr "workspace.tokens.shadow-remove-shadow")}] - (for [[input-type {:keys [label placeholder]}] (shadow-inputs)] - [:> shadow-input* - {:key (str input-type shadow-idx) - :input-type input-type - :label label - :placeholder placeholder - :shadow-idx shadow-idx - :default-value (get shadow input-type) - :on-update-value on-update-value - :token-resolve-result token-resolve-result - :errors-by-key errors-by-key - :on-external-update-value on-external-update-value - :shadow-color shadow-color}])])) - -(mf/defc shadow-value-inputs* - [{:keys [default-value on-update-value token-resolve-result update-composite-value] :as props}] - (let [shadows* (mf/use-state (or default-value [{}])) - shadows (deref shadows*) - shadows-count (count shadows) - composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result))) - - ;; Maintain a map of color states for each shadow to prevent reset on add/remove - shadow-colors* (mf/use-state {}) - shadow-colors (deref shadow-colors*) - - ;; Initialize color states for each shadow index - _ (mf/use-effect - (mf/deps shadows) - (fn [] - (doseq [[idx shadow] (d/enumerate shadows)] - (when-not (contains? shadow-colors idx) - (let [resolved-color (get-in token-resolve-result [:resolved-value idx :color]) - initial-color (or resolved-color (get shadow :color) "")] - (swap! shadow-colors* assoc idx initial-color)))))) - - ;; Define on-external-update-value here where we have access to on-update-value - on-external-update-value - (mf/use-callback - (mf/deps on-update-value shadow-colors*) - (fn [token-type-at-index value] - (let [[idx token-type] token-type-at-index - e (js-obj)] - ;; Update shadow color state if this is a color update - (when (= token-type :color) - (swap! shadow-colors* assoc idx value)) - (obj/set! e "tokenTypeAtIndex" token-type-at-index) - (obj/set! e "target" #js {:value value}) - (on-update-value e)))) - - on-add-shadow - (mf/use-fn - (mf/deps shadows update-composite-value) - (fn [] - (update-composite-value - (fn [state] - (let [new-state (update state :composite (fnil conj []) {})] - (reset! shadows* (:composite new-state)) - new-state))))) - - on-remove-shadow - (mf/use-fn - (mf/deps shadows update-composite-value) - (fn [idx] - (update-composite-value - (fn [state] - (let [new-state (update state :composite d/remove-at-index idx)] - (reset! shadows* (:composite new-state)) - new-state)))))] - [:div {:class (stl/css :nested-input-row)} - (for [[shadow-idx shadow] (d/enumerate shadows) - :let [is-remove-disabled (= shadows-count 1) - key (str shadows-count shadow-idx) - errors-by-key (when composite-token? - (sd/collect-shadow-errors token-resolve-result shadow-idx))]] - [:div {:key key - :class (stl/css :nested-input-row)} - [:> shadow-input-fields* - {:is-remove-disabled is-remove-disabled - :shadow-idx shadow-idx - :on-add-shadow on-add-shadow - :on-remove-shadow on-remove-shadow - :shadow shadow - :on-update-value on-update-value - :token-resolve-result token-resolve-result - :errors-by-key errors-by-key - :on-external-update-value on-external-update-value - :shadow-color (get shadow-colors shadow-idx "")}]])])) - (mf/defc shadow-form* [{:keys [token] :rest props}] (let [on-get-token-value @@ -1217,85 +765,6 @@ :on-get-token-value on-get-token-value :update-composite-backup-value update-composite-backup-value})])) -(mf/defc font-selector-wrapper* - [{:keys [font input-ref on-select-font on-close-font-selector]}] - (let [current-font* (mf/use-state (or font - (some-> (mf/ref-val input-ref) - (dom/get-value) - (cto/split-font-family) - (first) - (fonts/find-font-family)))) - current-font (deref current-font*)] - [:div {:class (stl/css :font-select-wrapper)} - [:> font-selector* {:current-font current-font - :on-select on-select-font - :on-close on-close-font-selector - :full-size true}]])) - -(mf/defc font-picker-combobox* - [{:keys [default-value label aria-label input-ref on-blur on-update-value on-external-update-value token-resolve-result placeholder]}] - (let [font* (mf/use-state (fonts/find-font-family default-value)) - font (deref font*) - set-font (mf/use-fn - (mf/deps font) - #(reset! font* %)) - - font-selector-open* (mf/use-state false) - font-selector-open? (deref font-selector-open*) - - on-close-font-selector - (mf/use-fn - (fn [] - (reset! font-selector-open* false))) - - 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?)))) - - on-select-font - (mf/use-fn - (mf/deps on-external-update-value set-font font) - (fn [{:keys [family] :as font}] - (when font - (set-font font) - (on-external-update-value family)))) - - on-update-value' - (mf/use-fn - (mf/deps on-update-value set-font) - (fn [value] - (set-font nil) - (on-update-value value))) - - 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"}])] - [:* - [:> input-token* - {:placeholder (or placeholder (tr "workspace.tokens.token-font-family-value-enter")) - :label label - :aria-label aria-label - :default-value (or (:name font) default-value) - :ref input-ref - :on-blur on-blur - :on-change on-update-value' - :icon i/text-font-family - :slot-end font-selector-button - :token-resolve-result token-resolve-result}] - (when font-selector-open? - [:> font-selector-wrapper* {:font font - :input-ref input-ref - :on-select-font on-select-font - :on-close-font-selector on-close-font-selector}])])) - (mf/defc font-family-form* [{:keys [token] :rest props}] (let [on-value-resolve @@ -1327,98 +796,6 @@ (mf/spread-props props {:token token :input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})]) -(def ^:private typography-inputs - #(d/ordered-map - :font-family - {:label (tr "workspace.tokens.token-font-family-value") - :icon i/text-font-family - :placeholder (tr "workspace.tokens.token-font-family-value-enter")} - :font-size - {:label "Font Size" - :icon i/text-font-size - :placeholder (tr "workspace.tokens.font-size-value-enter")} - :font-weight - {:label "Font Weight" - :icon i/text-font-weight - :placeholder (tr "workspace.tokens.font-weight-value-enter")} - :line-height - {:label "Line Height" - :icon i/text-lineheight - :placeholder (tr "workspace.tokens.line-height-value-enter")} - :letter-spacing - {:label "Letter Spacing" - :icon i/text-letterspacing - :placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite")} - :text-case - {:label "Text Case" - :icon i/text-mixed - :placeholder (tr "workspace.tokens.text-case-value-enter")} - :text-decoration - {:label "Text Decoration" - :icon i/text-underlined - :placeholder (tr "workspace.tokens.text-decoration-value-enter")})) - -(mf/defc typography-value-inputs* - [{:keys [default-value on-blur on-update-value token-resolve-result]}] - (let [composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result))) - 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 [[token-type {:keys [label placeholder icon]}] typography-inputs] - (let [value (get default-value token-type) - resolved (get-in token-resolve-result [:resolved-value token-type]) - errors (get errors-by-key token-type) - - should-show? (or (and (some? resolved) - (not= value (str resolved))) - (seq errors)) - - token-prop (when (and composite-token? should-show?) - (d/without-nils - {:resolved-value (when-not (str/empty? resolved) resolved) - :errors errors})) - - input-ref (mf/use-ref) - - on-external-update-value - (mf/use-fn - (mf/deps on-update-value) - (fn [next-value] - (let [element (mf/ref-val input-ref)] - (dom/set-value! element next-value) - (on-update-value #js {:target element - :tokenType :font-family})))) - - on-change - (mf/use-fn - (mf/deps token-type) - ;; Passing token-type via event to prevent deep function adapting & passing of type - (fn [event] - (-> (obj/set! event "tokenType" token-type) - (on-update-value))))] - - [:div {:key (str token-type) - :class (stl/css :input-row)} - (case token-type - :font-family - [:> font-picker-combobox* - {:aria-label label - :placeholder placeholder - :input-ref input-ref - :default-value (when value (cto/join-font-family value)) - :on-blur on-blur - :on-update-value on-change - :on-external-update-value on-external-update-value - :token-resolve-result token-prop}] - [:> input-token* - {:aria-label label - :placeholder placeholder - :default-value value - :on-blur on-blur - :icon icon - :on-change on-change - :token-resolve-result token-prop}])]))])) - (mf/defc typography-form* [{:keys [token] :rest props}] (let [on-get-token-value diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/form.scss index 08164ffa83..d5fd88dcc3 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/form.scss @@ -41,34 +41,6 @@ gap: var(--sp-xs); } -.nested-input-row { - display: flex; - flex-direction: column; - gap: var(--sp-m); -} - -.typography-inputs-row { - display: flex; - flex-direction: column; - gap: var(--sp-m); -} - -.typography-inputs { - border-inline-start: $b-1 solid var(--color-accent-primary-muted); - padding-inline-start: var(--sp-m); -} - -.title-bar { - display: grid; - grid-template-columns: 1fr auto; -} -.title { - @include t.use-typography("body-small"); - color: var(--color-foreground-primary); - display: flex; - align-items: center; -} - .warning-name-change-notification-wrapper { margin-block-start: var(--sp-l); } diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/shadow.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/shadow.cljs new file mode 100644 index 0000000000..3df034685e --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/shadow.cljs @@ -0,0 +1,259 @@ +;; 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.shadow + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + + [app.common.types.token :as cto] + [app.main.data.style-dictionary :as sd] + + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i] + + [app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]] + [app.main.ui.workspace.tokens.management.create.shared.color-picker :refer [color-picker*]] + [app.util.i18n :refer [tr]] + [app.util.object :as obj] + [rumext.v2 :as mf])) + +(mf/defc inset-type-select* + {::mf/private true} + [{:keys [default-value shadow-idx label on-change]}] + (let [selected* (mf/use-state (or (str default-value) "false")) + selected (deref selected*) + + on-change + (mf/use-fn + (mf/deps on-change selected shadow-idx) + (fn [value e] + (obj/set! e "tokenValue" (if (= "true" value) true false)) + (on-change e) + (reset! selected* (str value))))] + [:div {:class (stl/css :input-row)} + [:div {:class (stl/css :inset-label)} label] + [:& radio-buttons {:selected selected + :on-change on-change + :name (str "inset-select-" shadow-idx)} + [:& radio-button {:value "false" + :title "false" + :icon "❌" + :id (str "inset-default-" shadow-idx)}] + [:& radio-button {:value "true" + :title "true" + :icon "✅" + :id (str "inset-false-" shadow-idx)}]]])) + +(def ^:private shadow-inputs + #(d/ordered-map + :offsetX + {:label (tr "workspace.tokens.shadow-x") + :placeholder (tr "workspace.tokens.shadow-x")} + :offsetY + {:label (tr "workspace.tokens.shadow-y") + :placeholder (tr "workspace.tokens.shadow-y")} + :blur + {:label (tr "workspace.tokens.shadow-blur") + :placeholder (tr "workspace.tokens.shadow-blur")} + :spread + {:label (tr "workspace.tokens.shadow-spread") + :placeholder (tr "workspace.tokens.shadow-spread")} + :color + {:label (tr "workspace.tokens.shadow-color") + :placeholder (tr "workspace.tokens.shadow-color")} + :inset + {:label (tr "workspace.tokens.shadow-inset") + :placeholder (tr "workspace.tokens.shadow-inset")})) + +(mf/defc shadow-color-picker-wrapper* + "Wrapper for color-picker* that passes shadow color state from parent. + Similar to color-form* but receives color state from shadow-value-inputs*." + {::mf/private true} + [{:keys [placeholder label default-value input-ref on-update-value on-external-update-value token-resolve-result shadow-color]}] + (let [;; Use the color state passed from parent (shadow-value-inputs*) + resolved-color (get token-resolve-result :resolved-value) + color (or shadow-color resolved-color default-value "") + + custom-input-token-value-props + (mf/use-memo + (mf/deps color) + (fn [] + {:color color}))] + + [:> color-picker* + {:placeholder placeholder + :label label + :default-value default-value + :input-ref input-ref + :on-update-value on-update-value + :on-external-update-value on-external-update-value + :custom-input-token-value-props custom-input-token-value-props + :token-resolve-result token-resolve-result}])) + +(mf/defc shadow-input* + {::mf/private true} + [{:keys [default-value label placeholder shadow-idx input-type on-update-value on-external-update-value token-resolve-result errors-by-key shadow-color]}] + (let [color-input-ref (mf/use-ref) + + on-change + (mf/use-fn + (mf/deps shadow-idx input-type on-update-value) + (fn [e] + (-> (obj/set! e "tokenTypeAtIndex" [shadow-idx input-type]) + (on-update-value)))) + + on-external-update-value' + (mf/use-fn + (mf/deps shadow-idx input-type on-external-update-value) + (fn [v] + (on-external-update-value [shadow-idx input-type] v))) + + resolved (get-in token-resolve-result [:resolved-value shadow-idx input-type]) + + errors (get errors-by-key input-type) + + should-show? (or (some? resolved) (seq errors)) + + token-prop (when should-show? + (d/without-nils + {:resolved-value resolved + :errors errors}))] + (case input-type + :inset + [:> inset-type-select* + {:default-value default-value + :shadow-idx shadow-idx + :label label + :on-change on-change}] + :color + [:> shadow-color-picker-wrapper* + {:placeholder placeholder + :label label + :default-value default-value + :input-ref color-input-ref + :on-update-value on-change + :on-external-update-value on-external-update-value' + :token-resolve-result token-prop + :shadow-color shadow-color + :data-testid (str "shadow-color-input-" shadow-idx)}] + [:div {:class (stl/css :input-row) + :data-testid (str "shadow-" (name input-type) "-input-" shadow-idx)} + [:> input-token* + {:label label + :placeholder placeholder + :default-value default-value + :on-change on-change + :token-resolve-result token-prop}]]))) + +(mf/defc shadow-input-fields* + {::mf/private true} + [{:keys [shadow shadow-idx on-remove-shadow on-add-shadow is-remove-disabled on-update-value token-resolve-result errors-by-key on-external-update-value shadow-color] :as props}] + (let [on-remove-shadow + (mf/use-fn + (mf/deps shadow-idx on-remove-shadow) + #(on-remove-shadow shadow-idx))] + [:div {:data-testid (str "shadow-input-fields-" shadow-idx)} + [:> icon-button* {:icon i/add + :type "button" + :on-click on-add-shadow + :data-testid (str "shadow-add-button-" shadow-idx) + :aria-label (tr "workspace.tokens.shadow-add-shadow")}] + [:> icon-button* {:variant "ghost" + :type "button" + :icon i/remove + :on-click on-remove-shadow + :disabled is-remove-disabled + :data-testid (str "shadow-remove-button-" shadow-idx) + :aria-label (tr "workspace.tokens.shadow-remove-shadow")}] + (for [[input-type {:keys [label placeholder]}] (shadow-inputs)] + [:> shadow-input* + {:key (str input-type shadow-idx) + :input-type input-type + :label label + :placeholder placeholder + :shadow-idx shadow-idx + :default-value (get shadow input-type) + :on-update-value on-update-value + :token-resolve-result token-resolve-result + :errors-by-key errors-by-key + :on-external-update-value on-external-update-value + :shadow-color shadow-color}])])) + +(mf/defc shadow-value-inputs* + [{:keys [default-value on-update-value token-resolve-result update-composite-value] :as props}] + (let [shadows* (mf/use-state (or default-value [{}])) + shadows (deref shadows*) + shadows-count (count shadows) + composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result))) + + ;; Maintain a map of color states for each shadow to prevent reset on add/remove + shadow-colors* (mf/use-state {}) + shadow-colors (deref shadow-colors*) + + ;; Define on-external-update-value here where we have access to on-update-value + on-external-update-value + (mf/use-fn + (mf/deps on-update-value shadow-colors*) + (fn [token-type-at-index value] + (let [[idx token-type] token-type-at-index + e (js-obj)] + ;; Update shadow color state if this is a color update + (when (= token-type :color) + (swap! shadow-colors* assoc idx value)) + (obj/set! e "tokenTypeAtIndex" token-type-at-index) + (obj/set! e "target" #js {:value value}) + (on-update-value e)))) + + on-add-shadow + (mf/use-fn + (mf/deps shadows update-composite-value) + (fn [] + (update-composite-value + (fn [state] + (let [new-state (update state :composite (fnil conj []) {})] + (reset! shadows* (:composite new-state)) + new-state))))) + + on-remove-shadow + (mf/use-fn + (mf/deps shadows update-composite-value) + (fn [idx] + (update-composite-value + (fn [state] + (let [new-state (update state :composite d/remove-at-index idx)] + (reset! shadows* (:composite new-state)) + new-state)))))] + + (mf/use-effect + (mf/deps shadows) + (fn [] + (doseq [[idx shadow] (d/enumerate shadows)] + (when-not (contains? shadow-colors idx) + (let [resolved-color (get-in token-resolve-result [:resolved-value idx :color]) + initial-color (or resolved-color (get shadow :color) "")] + (swap! shadow-colors* assoc idx initial-color)))))) + + [:div {:class (stl/css :nested-input-row)} + (for [[shadow-idx shadow] (d/enumerate shadows) + :let [is-remove-disabled (= shadows-count 1) + key (str shadows-count shadow-idx) + errors-by-key (when composite-token? + (sd/collect-shadow-errors token-resolve-result shadow-idx))]] + [:div {:key key + :class (stl/css :nested-input-row)} + [:> shadow-input-fields* + {:is-remove-disabled is-remove-disabled + :shadow-idx shadow-idx + :on-add-shadow on-add-shadow + :on-remove-shadow on-remove-shadow + :shadow shadow + :on-update-value on-update-value + :token-resolve-result token-resolve-result + :errors-by-key errors-by-key + :on-external-update-value on-external-update-value + :shadow-color (get shadow-colors shadow-idx "")}]])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/shadow.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/shadow.scss new file mode 100644 index 0000000000..544a39db1a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/shadow.scss @@ -0,0 +1,17 @@ +// 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 + +.input-row { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.nested-input-row { + display: flex; + flex-direction: column; + gap: var(--sp-m); +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/shared/color_picker.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/color_picker.cljs new file mode 100644 index 0000000000..dd89c45cd1 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/color_picker.cljs @@ -0,0 +1,128 @@ +(ns app.main.ui.workspace.tokens.management.create.shared.color-picker + (:require + [app.common.types.color :as c] + [app.main.data.tinycolor :as tinycolor] + [app.main.ui.workspace.colorpicker :as colorpicker] + [app.main.ui.workspace.colorpicker.ramp :refer [ramp-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-token* token-value-hint*]] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +;; FIXME: this function has confusing name +(defn- hex->value + [hex] + (when-let [tc (tinycolor/valid-color hex)] + (let [hex (tinycolor/->hex-string tc) + alpha (tinycolor/alpha tc) + [r g b] (c/hex->rgb hex) + [h s v] (c/hex->hsv hex)] + {:hex hex + :r r :g g :b b + :h h :s s :v v + :alpha alpha}))) + +(mf/defc ramp* + [{:keys [color on-change]}] + (let [wrapper-node-ref (mf/use-ref nil) + dragging-ref (mf/use-ref false) + + on-start-drag + (mf/use-fn #(mf/set-ref-val! dragging-ref true)) + + on-finish-drag + (mf/use-fn #(mf/set-ref-val! dragging-ref false)) + + internal-color* + (mf/use-state #(hex->value color)) + + internal-color + (deref internal-color*) + + on-change' + (mf/use-fn + (mf/deps on-change) + (fn [{:keys [hex alpha] :as selector-color}] + (let [dragging? (mf/ref-val dragging-ref)] + (when-not (and dragging? hex) + (reset! internal-color* selector-color) + (on-change hex alpha)))))] + + (mf/use-effect + (mf/deps color) + (fn [] + ;; Update internal color when user changes input value + (when-let [color (tinycolor/valid-color color)] + (when-not (= (tinycolor/->hex-string color) (:hex internal-color)) + (reset! internal-color* (hex->value color)))))) + + (colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color) + [:div {:ref wrapper-node-ref} + [:> ramp-selector* + {:color internal-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag + :on-change on-change'}]])) + +(mf/defc color-picker* + [{: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*) + + on-click-swatch + (mf/use-fn + (mf/deps color-ramp-open? on-display-colorpicker) + (fn [] + (let [open? (not color-ramp-open?)] + (reset! color-ramp-open* open?) + (when on-display-colorpicker + (on-display-colorpicker open?))))) + + swatch + (mf/html + [:> input-token-color-bullet* + {:color color + :on-click on-click-swatch}]) + + on-change' + (mf/use-fn + (mf/deps color on-external-update-value) + (fn [hex-value alpha] + (let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field + prev-input-color (some-> (dom/get-value (mf/ref-val input-ref)) + (tinycolor/valid-color)) + ;; If the input is a reference we will take the format from the computed value + prev-computed-color (when-not prev-input-color + (some-> color (tinycolor/valid-color))) + prev-format (some-> (or prev-input-color prev-computed-color) + (tinycolor/color-format)) + to-rgba? (and + (< alpha 1) + (or (= prev-format "hex") (not prev-format))) + to-hex? (and (not prev-format) (= alpha 1)) + format (cond + to-rgba? "rgba" + to-hex? "hex" + prev-format prev-format + :else "hex") + color-value (-> (tinycolor/valid-color hex-value) + (tinycolor/set-alpha (or alpha 1)) + (tinycolor/->string format))] + (dom/set-value! (mf/ref-val input-ref) color-value) + (on-external-update-value color-value))))] + + [:* + [:> input-token* + {:placeholder placeholder + :label label + :default-value default-value + :ref input-ref + :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'}]) + [:> token-value-hint* {:result token-resolve-result}]])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/shared/composite_tabs.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/composite_tabs.cljs new file mode 100644 index 0000000000..bb5c6e4fe4 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/composite_tabs.cljs @@ -0,0 +1,105 @@ +(ns app.main.ui.workspace.tokens.management.create.shared.composite-tabs + (:require-macros [app.main.style :as stl]) + (:require + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.icons :as deprecated-icon] + [app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(mf/defc composite-reference-input* + {::mf/private true} + [{:keys [default-value on-blur on-update-value token-resolve-result reference-label reference-icon is-reference-fn]}] + [:> input-token* + {:aria-label (tr "labels.reference") + :placeholder reference-label + :icon reference-icon + :default-value (when (is-reference-fn default-value) default-value) + :on-blur on-blur + :on-change on-update-value + :token-resolve-result (when (or + (:errors token-resolve-result) + (string? (:value token-resolve-result))) + token-resolve-result)}]) + +(mf/defc composite-tabs* + [{:keys [default-value + on-update-value + on-external-update-value + on-value-resolve + clear-resolve-value + custom-input-token-value-props] + :rest props}] + (let [;; Active Tab State + {:keys [active-tab + composite-tab + is-reference-fn + reference-icon + reference-label + set-active-tab + title + update-composite-backup-value]} custom-input-token-value-props + reference-tab-active? (= :reference active-tab) + ;; Backup value ref + ;; Used to restore the previously entered value when switching tabs + ;; Uses ref to not trigger state updates during update + backup-state-ref (mf/use-var + (if reference-tab-active? + {:reference default-value} + {:composite default-value})) + default-value (get @backup-state-ref active-tab) + + on-toggle-tab + (mf/use-fn + (mf/deps active-tab on-external-update-value on-value-resolve clear-resolve-value) + (fn [] + (let [next-tab (if (= active-tab :composite) :reference :composite)] + ;; Clear the resolved value so it wont show up before the next-tab value has resolved + (clear-resolve-value) + ;; Restore the internal value from backup + (on-external-update-value (get @backup-state-ref next-tab)) + (set-active-tab next-tab)))) + + update-composite-value + (mf/use-fn + (fn [f] + (clear-resolve-value) + (swap! backup-state-ref f) + (on-external-update-value (get @backup-state-ref :composite)))) + + ;; Store updated value in backup-state-ref + on-update-value' + (mf/use-fn + (mf/deps on-update-value reference-tab-active? update-composite-backup-value) + (fn [e] + (if reference-tab-active? + (swap! backup-state-ref assoc :reference (dom/get-target-val e)) + (swap! backup-state-ref update :composite #(update-composite-backup-value % e))) + (on-update-value e)))] + [:div {:class (stl/css :typography-inputs-row)} + [:div {:class (stl/css :title-bar)} + [:div {:class (stl/css :title)} title] + [:& radio-buttons {:selected (if reference-tab-active? "reference" "composite") + :on-change on-toggle-tab + :name "reference-composite-tab"} + [:& radio-button {:icon deprecated-icon/layers + :value "composite" + :title (tr "workspace.tokens.individual-tokens") + :id "composite-opt"}] + [:& radio-button {:icon deprecated-icon/tokens + :value "reference" + :title (tr "workspace.tokens.use-reference") + :id "reference-opt"}]]] + [:div {:class (stl/css :typography-inputs)} + (if reference-tab-active? + [:> composite-reference-input* + (mf/spread-props props {:default-value default-value + :on-update-value on-update-value' + :reference-icon reference-icon + :reference-label reference-label + :is-reference-fn is-reference-fn})] + [:> composite-tab + (mf/spread-props props {:default-value default-value + :on-update-value on-update-value' + :update-composite-value update-composite-value})])]])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/shared/composite_tabs.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/composite_tabs.scss new file mode 100644 index 0000000000..811bde866a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/composite_tabs.scss @@ -0,0 +1,31 @@ +// 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/_borders.scss" as *; + +.typography-inputs-row { + display: flex; + flex-direction: column; + gap: var(--sp-m); +} + +.title-bar { + display: grid; + grid-template-columns: 1fr auto; +} + +.title { + @include t.use-typography("body-small"); + color: var(--color-foreground-primary); + display: flex; + align-items: center; +} + +.typography-inputs { + border-inline-start: $b-1 solid var(--color-accent-primary-muted); + padding-inline-start: var(--sp-m); +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/shared/font_combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/font_combobox.cljs new file mode 100644 index 0000000000..00cac8d188 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/font_combobox.cljs @@ -0,0 +1,92 @@ +(ns app.main.ui.workspace.tokens.management.create.shared.font-combobox + (:require-macros [app.main.style :as stl]) + (:require + [app.common.types.token :as cto] + [app.main.fonts :as fonts] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]] + [app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(mf/defc font-selector-wrapper* + {::mf/private true} + [{:keys [font input-ref on-select-font on-close-font-selector]}] + (let [current-font* (mf/use-state (or font + (some-> (mf/ref-val input-ref) + (dom/get-value) + (cto/split-font-family) + (first) + (fonts/find-font-family)))) + current-font (deref current-font*)] + [:div {:class (stl/css :font-select-wrapper)} + [:> font-selector* {:current-font current-font + :on-select on-select-font + :on-close on-close-font-selector + :full-size true}]])) + +(mf/defc font-picker-combobox* + [{:keys [default-value label aria-label input-ref on-blur on-update-value on-external-update-value token-resolve-result placeholder]}] + (let [font* (mf/use-state (fonts/find-font-family default-value)) + font (deref font*) + set-font (mf/use-fn + (mf/deps font) + #(reset! font* %)) + + font-selector-open* (mf/use-state false) + font-selector-open? (deref font-selector-open*) + + on-close-font-selector + (mf/use-fn + (fn [] + (reset! font-selector-open* false))) + + 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?)))) + + on-select-font + (mf/use-fn + (mf/deps on-external-update-value set-font font) + (fn [{:keys [family] :as font}] + (when font + (set-font font) + (on-external-update-value family)))) + + on-update-value' + (mf/use-fn + (mf/deps on-update-value set-font) + (fn [value] + (set-font nil) + (on-update-value value))) + + 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"}])] + [:* + [:> input-token* + {:placeholder (or placeholder (tr "workspace.tokens.token-font-family-value-enter")) + :label label + :aria-label aria-label + :default-value (or (:name font) default-value) + :ref input-ref + :on-blur on-blur + :on-change on-update-value' + :icon i/text-font-family + :slot-end font-selector-button + :token-resolve-result token-resolve-result}] + (when font-selector-open? + [:> font-selector-wrapper* {:font font + :input-ref input-ref + :on-select-font on-select-font + :on-close-font-selector on-close-font-selector}])])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/shared/font_combobox.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/font_combobox.scss new file mode 100644 index 0000000000..c2df1b932c --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/shared/font_combobox.scss @@ -0,0 +1,13 @@ +// 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 + +.font-select-wrapper { + position: absolute; + inset: 0; + // This padding from the modal should be shared as a variable + // Need to set this or the font-select will cause scroll + bottom: var(--sp-xxxl); +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/typography_composite.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/typography_composite.cljs new file mode 100644 index 0000000000..cc4a253f68 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/typography_composite.cljs @@ -0,0 +1,107 @@ +(ns app.main.ui.workspace.tokens.management.create.typography-composite + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.types.token :as cto] + [app.main.data.style-dictionary :as sd] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token*]] + [app.main.ui.workspace.tokens.management.create.shared.font-combobox :refer [font-picker-combobox*]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [app.util.object :as obj] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + + +(def ^:private typography-inputs + #(d/ordered-map + :font-family + {:label (tr "workspace.tokens.token-font-family-value") + :icon i/text-font-family + :placeholder (tr "workspace.tokens.token-font-family-value-enter")} + :font-size + {:label "Font Size" + :icon i/text-font-size + :placeholder (tr "workspace.tokens.font-size-value-enter")} + :font-weight + {:label "Font Weight" + :icon i/text-font-weight + :placeholder (tr "workspace.tokens.font-weight-value-enter")} + :line-height + {:label "Line Height" + :icon i/text-lineheight + :placeholder (tr "workspace.tokens.line-height-value-enter")} + :letter-spacing + {:label "Letter Spacing" + :icon i/text-letterspacing + :placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite")} + :text-case + {:label "Text Case" + :icon i/text-mixed + :placeholder (tr "workspace.tokens.text-case-value-enter")} + :text-decoration + {:label "Text Decoration" + :icon i/text-underlined + :placeholder (tr "workspace.tokens.text-decoration-value-enter")})) + +(mf/defc typography-value-inputs* + [{:keys [default-value on-blur on-update-value token-resolve-result]}] + (let [composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result))) + 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 [[token-type {:keys [label placeholder icon]}] typography-inputs] + (let [value (get default-value token-type) + resolved (get-in token-resolve-result [:resolved-value token-type]) + errors (get errors-by-key token-type) + + should-show? (or (and (some? resolved) + (not= value (str resolved))) + (seq errors)) + + token-prop (when (and composite-token? should-show?) + (d/without-nils + {:resolved-value (when-not (str/empty? resolved) resolved) + :errors errors})) + + input-ref (mf/use-ref) + + on-external-update-value + (mf/use-fn + (mf/deps on-update-value) + (fn [next-value] + (let [element (mf/ref-val input-ref)] + (dom/set-value! element next-value) + (on-update-value #js {:target element + :tokenType :font-family})))) + + on-change + (mf/use-fn + (mf/deps token-type) + ;; Passing token-type via event to prevent deep function adapting & passing of type + (fn [event] + (-> (obj/set! event "tokenType" token-type) + (on-update-value))))] + + [:div {:key (str token-type) + :class (stl/css :input-row)} + (case token-type + :font-family + [:> font-picker-combobox* + {:aria-label label + :placeholder placeholder + :input-ref input-ref + :default-value (when value (cto/join-font-family value)) + :on-blur on-blur + :on-update-value on-change + :on-external-update-value on-external-update-value + :token-resolve-result token-prop}] + [:> input-token* + {:aria-label label + :placeholder placeholder + :default-value value + :on-blur on-blur + :icon icon + :on-change on-change + :token-resolve-result token-prop}])]))])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/typography_composite.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/typography_composite.scss new file mode 100644 index 0000000000..544a39db1a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/create/typography_composite.scss @@ -0,0 +1,17 @@ +// 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 + +.input-row { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.nested-input-row { + display: flex; + flex-direction: column; + gap: var(--sp-m); +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index d496e88f52..0c34b098a6 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -38,6 +38,7 @@ :opacity "percentage" :number "number" :rotation "rotation" + :shadow "drop-shadow" :spacing "padding-extended" :string "text-mixed" :stroke-width "stroke-size"