mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
♻️ Replace token forms (#7759)
* 🎉 Create dimensions form * 🎉 Create text-case form * 🎉 Create color form * ♻️ Remove unused code on form file
This commit is contained in:
@@ -307,7 +307,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
await nameField.pressSequentially(".changed");
|
await nameField.pressSequentially(".changed");
|
||||||
|
|
||||||
await nameField.press("Enter");
|
await tokensUpdateCreateModal.getByRole("button", {name: "Save"}).click();
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
|
||||||
@@ -501,8 +501,9 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
|
|
||||||
// Clearing the input field should pick hex
|
// Clearing the input field should pick hex
|
||||||
await valueField.fill("");
|
await valueField.fill("");
|
||||||
|
// TODO: We need to fix this translation
|
||||||
await expect(
|
await expect(
|
||||||
tokensUpdateCreateModal.getByText("Token value cannot be empty"),
|
tokensUpdateCreateModal.getByText("Empty field"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
|
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
|
||||||
await expect(valueField).toHaveValue(/^#[A-Fa-f\d]+$/);
|
await expect(valueField).toHaveValue(/^#[A-Fa-f\d]+$/);
|
||||||
|
|||||||
@@ -64,11 +64,13 @@
|
|||||||
[:size {:optional true} [:enum "small" "medium" "large"]]
|
[:size {:optional true} [:enum "small" "medium" "large"]]
|
||||||
[:active {:optional true} ::sm/boolean]
|
[:active {:optional true} ::sm/boolean]
|
||||||
[:has-errors {:optional true} [:maybe ::sm/boolean]]
|
[:has-errors {:optional true} [:maybe ::sm/boolean]]
|
||||||
|
[:show-tooltip {:optional true} [:maybe ::sm/boolean]]
|
||||||
|
[:tooltip-content {:optional true} ::sm/any]
|
||||||
[:on-click {:optional true} ::sm/fn]])
|
[:on-click {:optional true} ::sm/fn]])
|
||||||
|
|
||||||
(mf/defc swatch*
|
(mf/defc swatch*
|
||||||
{::mf/schema (sm/schema schema:swatch)}
|
{::mf/schema (sm/schema schema:swatch)}
|
||||||
[{:keys [background on-click size active class tooltip-content has-errors]
|
[{:keys [background class size active has-errors tooltip-content on-click show-tooltip]
|
||||||
:rest props}]
|
:rest props}]
|
||||||
(let [;; NOTE: this code is only relevant for storybook, because
|
(let [;; NOTE: this code is only relevant for storybook, because
|
||||||
;; storybook is unable to pass in a comfortable way a complex
|
;; storybook is unable to pass in a comfortable way a complex
|
||||||
@@ -84,6 +86,7 @@
|
|||||||
id? (some? (:ref-id background))
|
id? (some? (:ref-id background))
|
||||||
element-type (if read-only? "div" "button")
|
element-type (if read-only? "div" "button")
|
||||||
button-type (if (not read-only?) "button" nil)
|
button-type (if (not read-only?) "button" nil)
|
||||||
|
show-tooltip (if (some? show-tooltip) show-tooltip true)
|
||||||
size (or size "small")
|
size (or size "small")
|
||||||
active (or active false)
|
active (or active false)
|
||||||
gradient-type (-> background :gradient :type)
|
gradient-type (-> background :gradient :type)
|
||||||
@@ -117,29 +120,34 @@
|
|||||||
(mf/spread-props props {:class class
|
(mf/spread-props props {:class class
|
||||||
:on-click on-click
|
:on-click on-click
|
||||||
:type button-type
|
:type button-type
|
||||||
:aria-labelledby element-id})]
|
:aria-labelledby element-id})
|
||||||
|
children (mf/html
|
||||||
|
[:> element-type props
|
||||||
|
(cond
|
||||||
|
(some? gradient-type)
|
||||||
|
[:div {:class (stl/css :swatch-gradient)
|
||||||
|
:style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
|
||||||
|
|
||||||
[:> tooltip* {:content (if tooltip-content
|
(some? image)
|
||||||
tooltip-content
|
(let [uri (cfg/resolve-file-media image)]
|
||||||
(color-title background))
|
[:div {:class (stl/css :swatch-image)
|
||||||
:id element-id}
|
:style {:background-image (str/ffmt "url(%)" uri)}}])
|
||||||
[:> element-type props
|
has-errors
|
||||||
(cond
|
[:div {:class (stl/css :swatch-error)}]
|
||||||
(some? gradient-type)
|
:else
|
||||||
[:div {:class (stl/css :swatch-gradient)
|
[:div {:class (stl/css :swatch-opacity)}
|
||||||
:style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
|
[:div {:class (stl/css :swatch-solid-side)
|
||||||
|
:style {:background (uc/color->background (assoc background :opacity 1))}}]
|
||||||
|
[:div {:class (stl/css-case :swatch-opacity-side true
|
||||||
|
:swatch-opacity-side-transparency has-opacity?
|
||||||
|
:swatch-opacity-side-solid-color (not has-opacity?))
|
||||||
|
:style {"--solid-color-overlay" (str (uc/color->background background))}}]])])]
|
||||||
|
|
||||||
(some? image)
|
(if show-tooltip
|
||||||
(let [uri (cfg/resolve-file-media image)]
|
[:> tooltip* {:content (if tooltip-content
|
||||||
[:div {:class (stl/css :swatch-image)
|
tooltip-content
|
||||||
:style {:background-image (str/ffmt "url(%)" uri)}}])
|
(color-title background))
|
||||||
has-errors
|
:id element-id}
|
||||||
[:div {:class (stl/css :swatch-error)}]
|
children]
|
||||||
:else
|
|
||||||
[:div {:class (stl/css :swatch-opacity)}
|
children)))
|
||||||
[:div {:class (stl/css :swatch-solid-side)
|
|
||||||
:style {:background (uc/color->background (assoc background :opacity 1))}}]
|
|
||||||
[:div {:class (stl/css-case :swatch-opacity-side true
|
|
||||||
:swatch-opacity-side-transparency has-opacity?
|
|
||||||
:swatch-opacity-side-solid-color (not has-opacity?))
|
|
||||||
:style {"--solid-color-overlay" (str (uc/color->background background))}}]])]]))
|
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
;; 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.color
|
||||||
|
(: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.form-color-input-token :refer [form-color-input-token*]]
|
||||||
|
[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]
|
||||||
|
(or token {:type :color}))
|
||||||
|
|
||||||
|
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)}
|
||||||
|
[:> form-color-input-token*
|
||||||
|
{: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,58 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
;; 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.dimensions
|
||||||
|
(: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.form-input-token :refer [form-input-token*]]
|
||||||
|
[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]
|
||||||
|
(or token {:type :dimensions}))
|
||||||
|
|
||||||
|
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)}
|
||||||
|
[:> form-input-token*
|
||||||
|
{: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,58 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.common.files.tokens :as cft]
|
[app.common.files.tokens :as cft]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.types.color :as c]
|
[app.common.types.color :as c]
|
||||||
@@ -37,8 +36,11 @@
|
|||||||
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
|
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
|
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
|
||||||
[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.dimensions :as dimensions]
|
||||||
[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.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.functions :as uf]
|
[app.util.functions :as uf]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
@@ -97,9 +99,6 @@
|
|||||||
(when (cto/token-value-self-reference? token-name token-value)
|
(when (cto/token-value-self-reference? token-name token-value)
|
||||||
(wte/get-error-code :error.token/direct-self-reference)))
|
(wte/get-error-code :error.token/direct-self-reference)))
|
||||||
|
|
||||||
(defn check-token-self-reference [token]
|
|
||||||
(check-self-reference (:name token) (:value token)))
|
|
||||||
|
|
||||||
(defn validate-resolve-token
|
(defn validate-resolve-token
|
||||||
[token prev-token tokens]
|
[token prev-token tokens]
|
||||||
(let [token (cond-> token
|
(let [token (cond-> token
|
||||||
@@ -911,40 +910,6 @@
|
|||||||
:on-change on-change'}])
|
:on-change on-change'}])
|
||||||
[:> token-value-hint* {:result token-resolve-result}]]))
|
[:> 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))
|
|
||||||
color (deref color*)
|
|
||||||
on-value-resolve (mf/use-fn
|
|
||||||
(mf/deps color)
|
|
||||||
(fn [value]
|
|
||||||
(reset! color* value)
|
|
||||||
value))
|
|
||||||
|
|
||||||
custom-input-token-value-props
|
|
||||||
(mf/use-memo
|
|
||||||
(mf/deps color on-display-colorpicker)
|
|
||||||
(fn []
|
|
||||||
{:color color
|
|
||||||
:on-display-colorpicker on-display-colorpicker}))
|
|
||||||
|
|
||||||
on-get-token-value
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [e]
|
|
||||||
(let [value (dom/get-target-val e)]
|
|
||||||
(if (tinycolor/hex-without-hash-prefix? value)
|
|
||||||
(let [hex-value (dm/str "#" value)]
|
|
||||||
(dom/set-value! (dom/get-target e) hex-value)
|
|
||||||
hex-value)
|
|
||||||
value))))]
|
|
||||||
|
|
||||||
[:> form*
|
|
||||||
(mf/spread-props props {:token token
|
|
||||||
:on-get-token-value on-get-token-value
|
|
||||||
:on-value-resolve on-value-resolve
|
|
||||||
:custom-input-token-value color-picker*
|
|
||||||
:custom-input-token-value-props custom-input-token-value-props})]))
|
|
||||||
|
|
||||||
(mf/defc shadow-color-picker-wrapper*
|
(mf/defc shadow-color-picker-wrapper*
|
||||||
"Wrapper for color-picker* that passes shadow color state from parent.
|
"Wrapper for color-picker* that passes shadow color state from parent.
|
||||||
Similar to color-form* but receives color state from shadow-value-inputs*."
|
Similar to color-form* but receives color state from shadow-value-inputs*."
|
||||||
@@ -1309,12 +1274,6 @@
|
|||||||
:on-value-resolve on-value-resolve
|
:on-value-resolve on-value-resolve
|
||||||
:validate-token validate-font-family-token})]))
|
:validate-token validate-font-family-token})]))
|
||||||
|
|
||||||
(mf/defc text-case-form*
|
|
||||||
[{:keys [token] :rest props}]
|
|
||||||
[:> form*
|
|
||||||
(mf/spread-props props {:token token
|
|
||||||
:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")})])
|
|
||||||
|
|
||||||
(mf/defc text-decoration-form*
|
(mf/defc text-decoration-form*
|
||||||
[{:keys [token] :rest props}]
|
[{:keys [token] :rest props}]
|
||||||
[:> form*
|
[:> form*
|
||||||
@@ -1481,12 +1440,13 @@
|
|||||||
:token token})]
|
:token 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* 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]
|
||||||
:border-radius [:> border-radius/form* props]
|
:border-radius [:> border-radius/form* props]
|
||||||
|
:dimensions [:> dimensions/form* props]
|
||||||
[:> form* props])))
|
[:> form* props])))
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
;; 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.form-color-input-token
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.types.color :as cl]
|
||||||
|
[app.common.types.tokens-lib :as ctob]
|
||||||
|
[app.main.data.style-dictionary :as sd]
|
||||||
|
[app.main.data.tinycolor :as tinycolor]
|
||||||
|
[app.main.ui.ds.controls.input :refer [input*]]
|
||||||
|
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
|
||||||
|
[app.main.ui.forms :as fc]
|
||||||
|
[app.main.ui.workspace.colorpicker :as colorpicker]
|
||||||
|
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-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 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)}))))))))
|
||||||
|
|
||||||
|
(defn- hex->color-obj
|
||||||
|
[hex]
|
||||||
|
(when-let [tc (tinycolor/valid-color hex)]
|
||||||
|
(let [hex (tinycolor/->hex-string tc)
|
||||||
|
alpha (tinycolor/alpha tc)
|
||||||
|
[r g b] (cl/hex->rgb hex)
|
||||||
|
[h s v] (cl/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->color-obj 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->color-obj 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 form-color-input-token*
|
||||||
|
[{:keys [name tokens token] :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] "")
|
||||||
|
|
||||||
|
resolved-value
|
||||||
|
(get-in @form [:data resolved-input-name] "")
|
||||||
|
|
||||||
|
hex (if (tinycolor/valid-color resolved-value)
|
||||||
|
(tinycolor/->hex-string (tinycolor/valid-color resolved-value))
|
||||||
|
"#8f9da3")
|
||||||
|
|
||||||
|
alpha (if (tinycolor/valid-color resolved-value)
|
||||||
|
(tinycolor/alpha (tinycolor/valid-color resolved-value))
|
||||||
|
1)
|
||||||
|
|
||||||
|
resolve-stream
|
||||||
|
(mf/with-memo [token]
|
||||||
|
(if-let [value (:value token)]
|
||||||
|
(rx/behavior-subject value)
|
||||||
|
(rx/subject)))
|
||||||
|
|
||||||
|
hint*
|
||||||
|
(mf/use-state {})
|
||||||
|
|
||||||
|
hint
|
||||||
|
(deref hint*)
|
||||||
|
|
||||||
|
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?)
|
||||||
|
(fn []
|
||||||
|
(let [open? (not color-ramp-open?)]
|
||||||
|
(reset! color-ramp-open* open?))))
|
||||||
|
|
||||||
|
swatch
|
||||||
|
(mf/html
|
||||||
|
[:> swatch*
|
||||||
|
{:background {:color hex :opacity alpha}
|
||||||
|
:show-tooltip false
|
||||||
|
:data-testid "token-form-color-bullet"
|
||||||
|
:class (stl/css :slot-start)
|
||||||
|
:on-click on-click-swatch}])
|
||||||
|
|
||||||
|
on-change-value
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps resolve-stream input-name value)
|
||||||
|
(fn [hex alpha]
|
||||||
|
(let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field
|
||||||
|
prev-input-color (some-> value
|
||||||
|
(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-> value (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)
|
||||||
|
(tinycolor/set-alpha (or alpha 1))
|
||||||
|
(tinycolor/->string format))]
|
||||||
|
(when (not= value color-value)
|
||||||
|
(fm/on-input-change form input-name color-value true)
|
||||||
|
(rx/push! resolve-stream color-value)))))
|
||||||
|
|
||||||
|
on-change
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps resolve-stream input-name)
|
||||||
|
(fn [event]
|
||||||
|
(let [raw-value (-> event dom/get-target dom/get-input-value)
|
||||||
|
value (if (tinycolor/hex-without-hash-prefix? raw-value)
|
||||||
|
(dm/str "#" raw-value)
|
||||||
|
raw-value)]
|
||||||
|
(fm/on-input-change form input-name value true)
|
||||||
|
(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-start swatch
|
||||||
|
: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" 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 color-ramp-open?
|
||||||
|
[:> ramp* {:color value :on-change on-change-value}])]))
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
;; 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.text-case
|
||||||
|
(: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.form-input-token :refer [form-input-token*]]
|
||||||
|
[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]
|
||||||
|
(or token {:type :text-case}))
|
||||||
|
|
||||||
|
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)}
|
||||||
|
[:> form-input-token*
|
||||||
|
{:placeholder (tr "workspace.tokens.text-case-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,58 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user